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

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 “.”

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 envsource ./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.1sqlparse==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.classProfile(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)defsave(self,*args,**kwargs):# newifnot self.slug:
            self.slug = slugify(self.user.username)returnsuper().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


classRegisterSerializer(serializers.ModelSerializer):classMeta:
        model = User
        fields =('id','username','password','first_name','last_name','email',)
        extra_kwargs ={'password':{'write_only':True},}defcreate(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


classUserSerializer(serializers.ModelSerializer):classMeta:
        model = User
        fields =('username','first_name','last_name','email',)classProfileSerializer(serializers.ModelSerializer):classMeta:
        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



classRegisterViewSet(generics.GenericAPIView):
    serializer_class = RegisterSerializer
    permission_classes =[AllowAny]defpost(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        refresh=Noneif 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),})classUserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes =[IsAuthenticated]
    filter_backends =[DjangoFilterBackend]
    filterset_fields =["id","username"]classProfileViewSet(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:3ENV PYTHONUNBUFFERED=1WORKDIR /app/backendCOPY requirements.txt /app/backend/RUN apt-get update 
    && apt-get -y install libpq-dev gccRUN pip install -r requirements.txtEXPOSE 8000CMD ["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-vitalsreportWebVitals();

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';functionApp(){return(<div className="App"><Switch><Route path="/auth"><AuthFeature /></Route><Route path="/" exact><HomeFeature /></Route></Switch></div>);}exportdefault 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 sentreturn config;},function(error){// Do something with request errorreturn 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 datareturn response;},function(error){// Any status codes that falls outside the range of 2xx cause this function to trigger// Do something with response errorreturn Promise.reject(error);});exportdefault 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
    },}exportdefault 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,})exportdefault store

Constants

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

const StorageKeys ={USER:'user',TOKEN:'access_token',REFRESH:'refresh_token',}exportdefault 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,}functionInputField(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>);}exportdefault 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,}functionPasswordField(props){const classes =useStyles();const{ form, name, label,disabled }= props;const{ formState:{ errors }}= form
    const hasError = errors[name]const[showPassword, setShowPassword]=useState(false);consthandleClickShowPassword=()=>{setShowPassword(!showPassword)};consthandleMouseDownPassword=(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>);}exportdefault 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 ={};functionLogin(props){const dispatch =useDispatch()const history =useHistory();constonSubmit=async(values)=>{
        console.log(values)try{const action =login(values)const resultAction =awaitdispatch(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>);}exportdefault 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,};functionLoginForm(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),});consthandleSubmit=(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>);}exportdefault 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 ={};functionRegister(props){const dispatch =useDispatch()const history =useHistory();constonSubmit=async(values)=>{try{const action =register(values)const resultAction =awaitdispatch(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>);}exportdefault 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,};functionRegisterForm(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),});consthandleSubmit=(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>);}exportdefault RegisterForm;
Page

Chỉnh sửa file LoginPage.jsx

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

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

Chỉnh sửa file RegisterPage.jsx

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

RegisterPage.propTypes ={};functionRegisterPage(props){return(<div style={{padding:"100px 0"}}><h1 style={{ textAlign:"center", marginBottom:"40px"}}>Register</h1><Register/></div>);}exportdefault 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 ={};functionAuthFeature(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>);}exportdefault 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';exportconst register =createAsyncThunk('users/register',async(payload)=>{//call api to registerconst response =await userApi.register(payload);//save data to local storageconst 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;})exportconst login =createAsyncThunk('users/login',async(payload)=>{//call api to registerconst 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
exportconst{ logout }= actions
exportdefault 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 ={};functionHomeFeature(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>);}exportdefault HomeFeature;

Docker

Chỉnh sửa Dockerfile

FROM node:12.18.1ENV NODE_ENV=productionWORKDIR /app/frontendCOPY package.json /app/frontend/RUN npm install --productionEXPOSE 3000CMD ["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:5restart: 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

Register

Local Storage

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

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ