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