+2

Authorization JWT trong Reactjs và Redux với Django build trên Docker

Giới thiệu

JSON Web Token (JWT)

JWT là một phương tiện đại diện cho các yêu cầu chuyển giao giữa hai bên Client – Server , các thông tin trong chuỗi JWT được định dạng bằng JSON . Trong đó chuỗi Token phải có 3 phần là header , phần payload và phần signature được ngăn bằng dấu “.” image.png

Ví dụ về JWT

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjI5NDg1MDcwLCJqdGkiOiJiNDRjMTQwNjQ4NGM0NmZjYjAwZmUzYWExNzQ2ZGVjNyIsInVzZXJfaWQiOjM1fQ.v0b6bMcbovoIkPhr_6PtVuRndpFN2QkGftxNtxjwjBo

Cấu trúc của một JWT rất đơn giản:

<base64-encoded header>.<base64-encoded payload>.<base64-encoded signature>

JWT là một công nghệ tuyệt vời để xác thực API và ủy quyền từ máy chủ đến máy chủ.

Sử dụng JWT để xác thực API

Ứng dụng nhiều nhất của JWT Token, và mục đích duy nhất bạn nên sử dụng JWT là dùng nó như một cơ chế xác thực API. Điều này hiện nay là quá phổ biến và được sử dụng rộng rãi, kể cả Google cũng sử dụng JWT để xác thực các APIs của họ. Ý tưởng rất đơn giản:

  • Bạn sẽ nhận được secret token từ service khi bạn thiết lập API
  • Ở phía client, bạn sẽ sử dụng secret token để kí tên và gắn nó vào token (hiện nay có rất nhiều thư viện hỗ trợ việc này)
  • Bạn sẽ gắn nó như một phần của API request và server sẽ biết nó là ai dựa trên request được ký với 1 unique identifier duy nhất:

Cài đặt

Giới thiệu về cấu trúc thư mục

login-jwt
├─ app
│  ├─ register
│  │  ├─ api
│  │  │  ├─ serializers.py
│  │  │  ├─ urls.py
│  │  |  └─ views.py 
│  │  ├─ __init__
│  │  ├─ admin.py
│  │  ├─ apps.py
│  │  ├─ models.py
│  │  ├─ tests.py
│  │  ├─ urls.py
│  │  └─ views.py 
│  ├─ app
│  │  ├─ __init__
│  │  ├─ asgi.py
│  │  ├─ setting.py
│  │  ├─ urls.py
│  │  └─ wsgi.py 
│  ├─ static
│  ├─ media
│  ├─ Dockerfile
│  ├─ manage.py
│  └─ requirements.txt
├─ frontend
│  ├─ build
│  ├─ node_modules
│  ├─ public
│  └─ src
│  |  ├─ api
│  |  │  ├─ axiosClient.js 
│  |  │  └─ userApi.js
│  |  ├─ app
│  |  │  └─ store.js
│  |  ├─ components
│  |  │  └─ form-control
│  |  │     ├─ InputField
│  |  │     │  └─ index.jsx
│  |  │     └─ PasswordField
│  |  │        └─ index.jsx
│  |  ├─ constants
│  |  |  └─ storage-keys.js
│  |  └─ features 
│  |  |  ├─ Auth
│  |  |  |  ├─ components
│  |  |  |  │  ├─ Login 
│  |  |  |  │  │  └─ index.jsx
│  |  |  |  │  ├─ LoginForm
│  |  |  |  │  │  └─ index.jsx
│  |  |  |  │  ├─ Register
│  |  |  |  │  │  └─ index.jsx
│  |  |  |  │  └─ RegisterForm
│  |  |  |  │     └─ index.jsx
│  |  |  |  └─ pages
│  |  |  |  │  ├─ LoginPage.jsx
│  |  |  |  │  └─ RegisterPage.jsx
│  |  |  |  ├─ index.jsx
│  |  |  |  └─ userSlice.js
│  |  |  └─ Home
│  |  |     └─ index.jsx
│  |  ├─ App.css
│  |  ├─ App.js
│  |  └─ index.js
│  ├─ Dockerfile
│  ├─ package-lock.json
│  └─ package.json
├─ .dockerignore
└─ docker-compose.yml

Backend (Django)

Install

cài đặt env nhớ mở bash trong visual code để active

python -m venv env
source ./env/Scripts/activate

Sau đó active env

pip install django
django-admin startproject app
django-admin startapp register

Cấu trúc

Cấu trúc file

login-jwt
├─ app
│  ├─ register
│  │  ├─ api
│  │  │  ├─ serializers.py
│  │  │  ├─ urls.py
│  │  |  └─ views.py 
│  │  ├─ __init__
│  │  ├─ admin.py
│  │  ├─ apps.py
│  │  ├─ models.py
│  │  ├─ tests.py
│  │  ├─ urls.py
│  │  └─ views.py 
│  ├─ app
│  │  ├─ __init__
│  │  ├─ asgi.py
│  │  ├─ setting.py
│  │  ├─ urls.py
│  │  └─ wsgi.py 
│  ├─ static
│  ├─ media
│  ├─ Dockerfile
│  ├─ manage.py
│  └─ requirements.txt
└─ frontend

Requirements

Sửa file requirements.txt

asgiref==3.4.1
autopep8==1.5.7
Django==3.2.6
django-cors-headers==3.8.0
django-filter==2.4.0
djangorestframework==3.12.4
djangorestframework-simplejwt==4.8.0
pycodestyle==2.7.0
PyJWT==2.1.0
pytz==2021.1
sqlparse==0.4.1
toml==0.10.2
psycopg2==2.8.6
psycopg2-binary>=2.8

Chạy lệnh sau đây

pip install -r requirements.txt

Setting

Chỉnh sửa file setting.py trong app



ALLOWED_HOSTS = ['*']

CORS_ORIGIN_ALLOW_ALL = True


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt',
    "corsheaders",
    'django_filters',
    'register',
]


MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
]


# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     }
# }

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'PASSWORD': 'postgres',
        'HOST': 'db',
        'PORT': 5432,
    }
}

STATIC_URL = '/static/'

STATIC_DIR = os.path.join(BASE_DIR, 'static')

STATICFILES_DIRS = [
    STATIC_DIR,
]

MEDIA_URL = '/media/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')


REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
        'rest_framework.permissions.AllowAny',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'AUTH_HEADER_TYPES': ('Bearer',),
}

Model

Chỉnh sửa models.py trong register

from django.db import models
from django.contrib.auth.models import User

# Create your models here.

from django.template.defaultfilters import slugify
from django.urls import reverse
# Create your models here.

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    phone_number = models.CharField(max_length=20)
    address = models.CharField(max_length=2000)
    slug = models.SlugField(max_length=2000, null=False)

    def save(self, *args, **kwargs):  # new
        if not self.slug:
            self.slug = slugify(self.user.username)
        return super().save(*args, **kwargs)

    def __str__(self) -> str:
        return self.user.username

Api

Chỉnh sửa serializers.py trong register/api

from django.urls import path, include
from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets
from register.models import Profile


class RegisterSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'password',
                  'first_name', 'last_name', 'email',)
        extra_kwargs = {
            'password': {'write_only': True},
        }

    def create(self, validated_data):
        user = User.objects.create_user(
            validated_data['username'],
            password=validated_data['password'],
            email=validated_data['email'],
            first_name=validated_data['first_name'],
            last_name=validated_data['last_name'])
        return user


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('username', 'first_name', 'last_name', 'email',)

class ProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = ('phone_number', 'address', 'user', 'slug',)

Chỉnh sửa file views.py trong thư mục register/api

from rest_framework import generics
from rest_framework import viewsets
from django.contrib.auth.models import User
from .serializers import ProfileSerializer, UserSerializer, RegisterSerializer
from rest_framework.permissions import IsAuthenticated, AllowAny
from django_filters.rest_framework import DjangoFilterBackend
from register.models import Profile
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from django.views.generic import ListView, DetailView



class RegisterViewSet(generics.GenericAPIView):
    serializer_class = RegisterSerializer
    permission_classes = [AllowAny]
    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        refresh=None
        if user:
            profile = Profile.objects.create(user=user, phone_number=request.data.get(
                "phone_number"), address=request.data.get("address"))
            profile.save()
        refresh = RefreshToken.for_user(user)
        return Response({
            "user": UserSerializer(user, context=self.get_serializer_context()).data,
            "refresh": str(refresh),
            "access": str(refresh.access_token),
        })


class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ["id", "username"]


class ProfileViewSet(viewsets.ModelViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ["user", "phone_number", "address", "slug"]


Chỉnh sửa file urls.py trong register/api

from rest_framework import routers
from .views import UserViewSet, RegisterViewSet, ProfileViewSet
from django.urls import path, include
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)


router = routers.DefaultRouter()
router.register("users", UserViewSet)
router.register("profile", ProfileViewSet)


urlpatterns = [
    path('', include(router.urls)),
    path('register/', RegisterViewSet.as_view()),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

App

Chỉnh sửa file urls.py trong app

from django.contrib import admin
from django.urls import path, include

from django.conf import settings
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include("register.api.urls")),
]

urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Docker

chỉnh sửa Dockerfile

FROM python:3
ENV PYTHONUNBUFFERED=1
WORKDIR /app/backend
COPY requirements.txt /app/backend/
RUN apt-get update \
    && apt-get -y install libpq-dev gcc
RUN pip install -r requirements.txt

EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Front-end

Cấu trúc file

login-jwt
├─ frontend
│  ├─ build
│  ├─ node_modules
│  ├─ public
│  └─ src
│  |  ├─ api
│  |  │  ├─ axiosClient.js 
│  |  │  └─ userApi.js
│  |  ├─ app
│  |  │  └─ store.js
│  |  ├─ components
│  |  │  └─ form-control
│  |  │     ├─ InputField
│  |  │     │  └─ index.jsx
│  |  │     └─ PasswordField
│  |  │        └─ index.jsx
│  |  ├─ constants
│  |  |  └─ storage-keys.js
│  |  └─ features 
│  |  |  ├─ Auth
│  |  |  |  ├─ components
│  |  |  |  │  ├─ Login 
│  |  |  |  │  │  └─ index.jsx
│  |  |  |  │  ├─ LoginForm
│  |  |  |  │  │  └─ index.jsx
│  |  |  |  │  ├─ Register
│  |  |  |  │  │  └─ index.jsx
│  |  |  |  │  └─ RegisterForm
│  |  |  |  │     └─ index.jsx
│  |  |  |  └─ pages
│  |  |  |  │  ├─ LoginPage.jsx
│  |  |  |  │  └─ RegisterPage.jsx
│  |  |  |  ├─ index.jsx
│  |  |  |  └─ userSlice.js
│  |  |  └─ Home
│  |  |     └─ index.jsx
│  |  ├─ App.css
│  |  ├─ App.js
│  |  └─ index.js
│  ├─ Dockerfile
│  ├─ package-lock.json
│  └─ package.json
├─ .dockerignore
└─ docker-compose.yml

Cài đặt

Cài đặt các thư viên sau đây

"dependencies": {
    "@hookform/resolvers": "^2.8.0",
    "@material-ui/core": "^4.12.3",
    "@material-ui/icons": "^4.11.2",
    "@reduxjs/toolkit": "^1.6.1",
    "axios": "^0.21.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-hook-form": "^7.12.2",
    "react-redux": "^7.2.4",
    "react-router-dom": "^5.2.0",
    "react-scripts": "4.0.3",
    "yup": "^0.32.9"
  }

App

Chỉnh sửa file index.js trong src

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter as Router } from "react-router-dom"
import store from './app/store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <Router>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </Router>
  </Provider>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Chỉnh sửa file app.js trong src

import { Route, Switch } from 'react-router-dom';
import './App.css';
import AuthFeature from './feature/Auth';
import HomeFeature from './feature/Home';

function App() {
  return (
    <div className="App">
        <Switch>
          <Route path="/auth">
            <AuthFeature />
          </Route>
          <Route path="/" exact>
            <HomeFeature />
          </Route>
        </Switch>
    </div>
  );
}

export default App;

Api

Chỉnh sửa axiosClient.js trong api

import axios from 'axios';

const axiosClient = axios.create({
    baseURL: 'http://127.0.0.1:8000/',
    headers: {
        'content-type': 'application/json',
    }
})


// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
}, function (error) {
    // Do something with request error
    return Promise.reject(error);
});

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
}, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
});


export default axiosClient

Chỉnh sửa userApi.js trong api

import StorageKeys from "../constants/storage-keys";
import axiosClient from "./axiosClient";

const userApi = {
    register(data) {
        const url = '/register/';
        return axiosClient.post(url, data);
    },
    login(data) {
        const url = '/api/token/';
        return axiosClient.post(url, data);
    },
    getUser(params) {
        const newParams = { ...params }
        const accessToken = localStorage.getItem(StorageKeys.TOKEN)
        const url = `users/`;
        const response = axiosClient.get(url, {
            params: { ...newParams },
            headers: {
                Authorization: `Bearer ${accessToken}`
            }
        });
        return response
    },
    getProfile(params) {
        const newParams = { ...params }
        const accessToken = localStorage.getItem(StorageKeys.TOKEN)
        const url = `profile/`;
        const response = axiosClient.get(url, {
            params: { ...newParams },
            headers: {
                Authorization: `Bearer ${accessToken}`
            }
        });
        return response
    },
}

export default userApi

App

Chỉnh sửa store.js trong app

import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../feature/Auth/userSlice'

const rootReducer  = {
    user: userReducer,
}

const store = configureStore({
  reducer: rootReducer,
})


export default store

Constants

Chỉnh sửa file storage-keys.js trong constants

const StorageKeys = {
    USER: 'user',
    TOKEN: 'access_token',
    REFRESH: 'refresh_token',
}

export default StorageKeys

Components

chỉnh sửa index.jsx trong form-control/InputField

import React from 'react';
import PropTypes from 'prop-types';
import { Controller } from "react-hook-form";
import TextField from '@material-ui/core/TextField';
import { FormHelperText } from '@material-ui/core';



InputField.propTypes = {
    form: PropTypes.object.isRequired,
    name: PropTypes.string.isRequired,
    label: PropTypes.string,
    disabled: PropTypes.bool,
};

InputField.defaultProps = {
    label: "",
    disabled: false,
}

function InputField(props) {

    const { form, name, label, disabled } = props;
    const { formState: { errors } } = form
    const hasError = errors[name]

    return (
        <div style={{ margin: "10px 0" }}>
            <Controller
                control={form.control}
                name={name}
                render={({ field: { onChange, onBlur, value, name, ref },
                    fieldState: { invalid, isTouched, isDirty, error },
                    formState, }) => {
                    return (
                        <TextField
                            onBlur={onBlur}
                            onChange={onChange}
                            inputRef={ref}
                            fullWidth
                            variant="outlined"
                            label={label}
                            error={!!hasError}
                            disabled={disabled}
                        />
                    )
                }}
            />
            <FormHelperText error={!!hasError}>
                {errors[name]?.message}
            </FormHelperText>
        </div>
    );
}

export default InputField;

chỉnh sửa index.jsx trong form-control/PasswordField

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Controller } from 'react-hook-form';
import { FormControl, FormHelperText, IconButton, InputAdornment, InputLabel, makeStyles, OutlinedInput } from '@material-ui/core';
import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff';
import clsx from 'clsx';


const useStyles = makeStyles((theme) => ({
    root: {
        display: 'flex',
        flexWrap: 'wrap',
    },
    margin: {
        margin: "10px 0",
    },
}));

PasswordField.propTypes = {
    form: PropTypes.object.isRequired,
    name: PropTypes.string.isRequired,
    label: PropTypes.string,
    disabled: PropTypes.bool,
};

PasswordField.defaultProps = {
    label: "",
    disabled: false,
}

function PasswordField(props) {
    const classes = useStyles();
    const { form, name, label,disabled } = props;

    const { formState: { errors } } = form
    const hasError = errors[name]

    const [showPassword, setShowPassword] = useState(false);

    const handleClickShowPassword = () => {
        setShowPassword(!showPassword)
    };

    const handleMouseDownPassword = (event) => {
        event.preventDefault();
    };

    return (
        <div>
            <Controller
                control={form.control}
                name={name}
                render={({ field: { onChange, onBlur, value, name, ref },
                    fieldState: { invalid, isTouched, isDirty, error },
                    formState, }) => {
                    return (
                        <FormControl className={clsx(classes.margin, classes.textField)} variant="outlined" fullWidth>
                            <InputLabel htmlFor="outlined-adornment-password" error={!!hasError}>{label}</InputLabel>
                            <OutlinedInput
                                onBlur={onBlur}
                                onChange={onChange}
                                inputRef={ref}
                                fullWidth
                                variant="outlined"
                                type={showPassword ? 'text' : 'password'}
                                endAdornment={
                                    <InputAdornment position="end">
                                        <IconButton
                                            aria-label="toggle password visibility"
                                            onClick={handleClickShowPassword}
                                            onMouseDown={handleMouseDownPassword}
                                            edge="end"
                                        >
                                            {showPassword ? <Visibility /> : <VisibilityOff />}
                                        </IconButton>
                                    </InputAdornment>
                                }
                                disabled={disabled}
                                error={!!hasError}
                                labelWidth={70}
                            />
                        </FormControl>
                    )
                }}
            />
            <FormHelperText style={{ color: "red"}}>
                {errors[name]?.message}
            </FormHelperText>
        </div>
    );
}

export default PasswordField;

Feature

Auth

Chỉnh sửa index.jsx trong Login

import React from 'react';
import LoginForm from '../LoginForm';
import { useDispatch } from 'react-redux';
import { login } from '../../userSlice';
import { unwrapResult } from '@reduxjs/toolkit';
import { useHistory } from 'react-router-dom';


Login.propTypes = {
    
};

function Login(props) {

    const dispatch = useDispatch()
    const history = useHistory();


    const onSubmit = async (values) => {
        console.log(values)
        try {
            const action = login(values)
            const resultAction = await dispatch(action);
            const user = unwrapResult(resultAction)
            console.log(user)
            history.push("/")
        } catch (error) {
            console.log("error: ", error)
        }
    }

    return (
        <div style={{ display: "flex", justifyContent: "center"}}>
            <LoginForm onSubmit={onSubmit}/>
        </div>
    );
}

export default Login;

Chỉnh sửa index.jsx trong LoginForm

import React from 'react';
import PropTypes from 'prop-types';
import { useForm } from "react-hook-form";
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from "yup";
import InputField from '../../../../components/form-control/InputField';
import Button from '@material-ui/core/Button';
import PasswordField from '../../../../components/form-control/PasswordField';

LoginForm.propTypes = {
    onSubmit: PropTypes.func,
};

function LoginForm(props) {

    const {onSubmit} = props

    const schema = yup.object().shape({
        username: yup.string().required(),
        password: yup.string().required(),
    });

    const form = useForm({
        defaultValue: {
            username: "",
            password: "",
        },
        resolver: yupResolver(schema),
    });

    const handleSubmit = (values) => {
        if(onSubmit){
            onSubmit(values)
        }
    }

    return (
        <div style={{ width: "500px"}}>
            <form onSubmit={form.handleSubmit(handleSubmit)} noValidate autoComplete="off">
                <InputField form={form} name="username" label="Tài Khoản" id="username"/>
                <PasswordField form={form} name="password" label="Mật khẩu" id="password"/>
                <Button variant="contained" color="primary" type="submit">Đăng nhập</Button>
            </form>
        </div>
    );
}

export default LoginForm;

Chỉnh sửa file index.jsx trong Register

import { unwrapResult } from '@reduxjs/toolkit';
import React from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { register } from '../../userSlice';
import RegisterForm from '../RegisterForm';


Register.propTypes = {
    
};

function Register(props) {

    const dispatch = useDispatch()
    const history = useHistory();

    const onSubmit = async (values) => {
        try {
            const action = register(values)
            const resultAction = await dispatch(action);
            const user = unwrapResult(resultAction);
            console.log(user)
            history.push("/")
        } catch (error) {
            console.log("error: ", error)
        }
    }

    return (
        <div style={{ display: "flex", justifyContent: "center"}}>
            <RegisterForm onSubmit={onSubmit}/>
        </div>
    );
}

export default Register;

Chỉnh sửa file index.jsx trong RegisterForm

import React from 'react';
import PropTypes from 'prop-types';
import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
import * as yup from "yup";
import InputField from '../../../../components/form-control/InputField';
import PasswordField from '../../../../components/form-control/PasswordField';
import { Button } from '@material-ui/core';


RegisterForm.propTypes = {
    onSubmit: PropTypes.func,
};

function RegisterForm(props) {

    const { onSubmit } = props

    const schema = yup.object().shape({
        username: yup.string()
            .required("Xin vui lòng nhập username."),
        email: yup.string()
            .required("Xin vui lòng nhập email.")
            .email("Xin vui lòng nhập một email"),
        password: yup.string()
            .required("Xin vui lòng nhập password.")
            .min(6, "Password phải có ít nhất 6 ký tự."),
        password1: yup.string()
            .required("Xin vui lòng nhập password")
            .oneOf([yup.ref('password')], 'Password không đúng.'),
        first_name: yup.string()
            .required("Xin vui lòng nhập Tên."),
        last_name: yup.string()
            .required("Xin vui lòng nhập Họ."),
        address: yup.string()
            .required("Xin vui lòng nhập địa chỉ."),
        phone_number: yup.string()
            .required("Xin vui lòng nhập số điện thoại."),
    });

    const form = useForm({
        defaultValue: {
            username: "",
            password: "",
            password1: "",
            email: "",
            first_name: "",
            last_name: "",
            address: "",
            phone_number: "",
        },
        resolver: yupResolver(schema),
    });

    const handleSubmit = (values) => {
        if (onSubmit) {
            onSubmit(values)
        }
    }

    return (
        <div style={{ width: "500px" }}>
            <form onSubmit={form.handleSubmit(handleSubmit)} noValidate autoComplete="off">
                <InputField form={form} name="username" label="Tài Khoản" id="username" />
                <PasswordField form={form} name="password" label="Mật khẩu" id="password" />
                <PasswordField form={form} name="password1" label="Mật khẩu" id="confirm_password" />
                <InputField form={form} name="email" label="Email" id="email" />
                <InputField form={form} name="first_name" label="Tên" id="first_name" />
                <InputField form={form} name="last_name" label="Họ" id="last_name" />
                <InputField form={form} name="phone_number" label="Số điện thoại" id="phone_number" />
                <InputField form={form} name="address" label="Địa chỉ" id="address" />
                <Button variant="contained" color="primary" type="submit">Đăng ký</Button>
            </form>
        </div>
    );
}

export default RegisterForm;
Page

Chỉnh sửa file LoginPage.jsx

import React from 'react';
import Login from '../components/Login';

LoginPage.propTypes = {
    
};

function LoginPage(props) {
    return (
        <div>
            <h1 style={{ textAlign: "center", marginBottom: "40px" }}>Login</h1>
            <Login/>
        </div>
    );
}

export default LoginPage;

Chỉnh sửa file RegisterPage.jsx

import React from 'react';
import Register from '../components/Register';

RegisterPage.propTypes = {

};

function RegisterPage(props) {
    return (
        <div style={{padding: "100px 0"}}>
            <h1 style={{ textAlign: "center", marginBottom: "40px" }}>Register</h1>
            <Register/>
        </div>
    );
}

export default RegisterPage;

Chỉnh sửa file index.jsx trong Auth

import React from 'react';
import { useSelector } from 'react-redux';
import { Route, Switch, useRouteMatch } from 'react-router-dom';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';

AuthFeature.propTypes = {

};

function AuthFeature(props) {

    const match = useRouteMatch()
    const user = useSelector(state => state.user.current)
    const isLogin = !!user.username

    return (
        <div>
            {!isLogin && (
                <Switch>
                    <Route path={`${match.url}/login`} exact>
                        <LoginPage />
                    </Route>
                    <Route path={`${match.url}/register`} exact>
                        <RegisterPage />
                    </Route>
                </Switch>
            )}
        </div>
    );
}

export default AuthFeature;

UserSlice

Chỉnh sửa file userSlice.js trong Auth

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import userApi from '../../api/userApi';
import StorageKeys from '../../constants/storage-keys';


export const register = createAsyncThunk(
    'users/register',
    async (payload) => {
        //call api to register
        const response = await userApi.register(payload);
        //save data to local storage
        const user = response.data.user
        localStorage.setItem(StorageKeys.TOKEN, response.data.access);
        localStorage.setItem(StorageKeys.REFRESH, response.data.refresh);
        localStorage.setItem(StorageKeys.USER, JSON.stringify(user.username));
        return user;
    }
)


export const login = createAsyncThunk(
    'users/login',
    async (payload) => {
        //call api to register
        const response = await userApi.login(payload);
        //save data to local storage
        localStorage.setItem(StorageKeys.TOKEN, response.data.access);
        localStorage.setItem(StorageKeys.REFRESH, response.data.refresh);
        const responseUser = await userApi.getUser({ username: payload.username })
        localStorage.setItem(StorageKeys.USER, JSON.stringify(responseUser.data[0]));
        return  responseUser.data[0];
    }
)

const userSlice = createSlice({
    name: 'user',
    initialState: {
        current: JSON.parse(localStorage.getItem(StorageKeys.USER)) || {},
        settings: {},
    },
    reducers: {
        logout(state){
            state.current = {}
        }
    },
    extraReducers: {
        //'user/register/fulfilled': () => {}
        [register.fulfilled]: (state, action) => {
            state.current = action.payload;
        },

        [login.fulfilled]: (state, action) => {
            state.current = action.payload;
        }
    }
})

const { actions, reducer } = userSlice
export const { logout } = actions
export default reducer

Home

Chỉnh sửa file index.jsx trong home

import React from 'react';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';

HomeFeature.propTypes = {

};

function HomeFeature(props) {

    const user = useSelector(state => state.user.current)
    const isLogin = !!user.username
    return (
        <div>
            <h1 style={{ textAlign: "center" }}>Home</h1>
            {!isLogin && (<>
                <div style={{ textAlign: "center" }}>
                    <Link to="auth/login">login</Link>
                </div>
                <div style={{ textAlign: "center" }}>
                    <Link to="auth/register">register</Link>
                </div>
            </>)}

            {isLogin && (<>
                <h2 style={{ textAlign: "center" }}>Hello {user.username}</h2>
            </>)}

        </div>
    );
}

export default HomeFeature;

Docker

Chỉnh sửa Dockerfile

FROM node:12.18.1
ENV NODE_ENV=production

WORKDIR /app/frontend
COPY package.json /app/frontend/
RUN npm install --production

EXPOSE 3000
CMD ["npm", "start"]

Docker

Chỉnh sửa file .dockerignore

node_modules

Chỉnh sửa docker-compose.yml

version: "3.9"

services:
  db:
    image: postgres
    volumes:
      - ./app/data/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always

  backend:
    build: ./app
    command: bash -c "python manage.py makemigrations && python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
    volumes:
      - ./app:/app/backend
    ports:
      - "8000:8000"
    depends_on:
      db:
        condition: service_healthy
    restart: on-failure

  frontend:
    build: ./frontend
    command: ["npm", "start"]
    volumes:
      - ./frontend:/app/frontend
      - node-modules:/app/frontend/node_modules
    ports:
      - "3000:3000"
    environment:
      - CHOKIDAR_USEPOLLING=true

volumes:
  node-modules: 

Khởi chạy

docker-compose build
docker-compose up

Demo

Login

image.png

Register

image.png

Local Storage

image.png

Bài viết đến đây là kết thúc. Chúc các bạn thành công


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.