26
loading...
This website collects cookies to deliver better user experience
virtualenv
here.virtualenv --python=/usr/bin/python3.8 venv
source venv/bin/activate
pip install django djangorestframework djangorestframework-simplejwt
django-admin startproject CoreRoot .
django-admin startapp core
__init__.py
and apps.py
.core
to the INSTALLED_APPS :
# CoreRoot/settings.py
...
'django.contrib.messages',
'django.contrib.staticfiles',
'core'
cd core && python ../manage.py startapp user
# CoreRoot/settings.py
...
'rest_framework',
'core',
'core.user'
# core/user/apps.py
from django.apps import AppConfig
class UserConfig(AppConfig):
name = 'core.user'
label = 'core_user'
__init__.py
file in core/user
directory.# core/user/__init__.py
default_app_config = 'core.user.apps.UserConfig'
AbstractBaseUser
. But we’ll also rewrite the UserManager
to customize the creation of a user in the database.settings.py
.# core/user/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
class UserManager(BaseUserManager):
def create_user(self, username, email, password=None, **kwargs):
"""Create and return a `User` with an email, phone number, username and password."""
if username is None:
raise TypeError('Users must have a username.')
if email is None:
raise TypeError('Users must have an email.')
user = self.model(username=username, email=self.normalize_email(email))
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, email, password):
"""
Create and return a `User` with superuser (admin) permissions.
"""
if password is None:
raise TypeError('Superusers must have a password.')
if email is None:
raise TypeError('Superusers must have an email.')
if username is None:
raise TypeError('Superusers must have an username.')
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.save(using=self._db)
return user
class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(db_index=True, max_length=255, unique=True)
email = models.EmailField(db_index=True, unique=True, null=True, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
objects = UserManager()
def __str__(self):
return f"{self.email}"
AUTH_USER_MODEL
.# CoreRoot/settings.py
...
AUTH_USER_MODEL = 'core_user.User'
...
querysets
or model instances in Python native objects that can be easily converted JSON/XML format, but Serializer also serializes JSON/XML to naive Python.# core/user/serializers.py
from core.user.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'is_active', 'created', 'updated']
read_only_field = ['is_active', 'created', 'updated']
# core/user/viewsets.py
from core.user.serializers import UserSerializer
from core.user.models import User
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework import filters
class UserViewSet(viewsets.ModelViewSet):
http_method_names = ['get']
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
filter_backends = [filters.OrderingFilter]
ordering_fields = ['updated']
ordering = ['-updated']
def get_queryset(self):
if self.request.user.is_superuser:
return User.objects.all()
def get_object(self):
lookup_field_value = self.kwargs[self.lookup_field]
obj = User.objects.get(lookup_field_value)
self.check_object_permissions(self.request, obj)
return obj
djangorestframework-simplejwt
to implement an access/refresh logic.rest_framework_simplejwt.authentication.JWTAuthentication
to the list of authentication classes in settings.py
:# CoreRoot/settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
}
djangorestframework-simplejwt
contributors, it’s very simple to read the code, understand how it works and extend it successfully.auth
in core
.serializer.py
which will contain the login and register serializers.
# core/auth/serializers.py
from rest_framework import serializers
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.settings import api_settings
from django.contrib.auth.models import update_last_login
from django.core.exceptions import ObjectDoesNotExist
from core.user.serializers import UserSerializer
from core.user.models import User
class LoginSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
data['user'] = UserSerializer(self.user).data
data['refresh'] = str(refresh)
data['access'] = str(refresh.access_token)
if api_settings.UPDATE_LAST_LOGIN:
update_last_login(None, self.user)
return data
class RegisterSerializer(UserSerializer):
password = serializers.CharField(max_length=128, min_length=8, write_only=True, required=True)
email = serializers.EmailField(required=True, write_only=True, max_length=128)
class Meta:
model = User
fields = ['id', 'username', 'email', 'password', 'is_active', 'created', 'updated']
def create(self, validated_data):
try:
user = User.objects.get(email=validated_data['email'])
except ObjectDoesNotExist:
user = User.objects.create_user(**validated_data)
return user
# core/auth/viewsets
from rest_framework.response import Response
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
from core.auth.serializers import LoginSerializer, RegistrationSerializer
class LoginViewSet(ModelViewSet, TokenObtainPairView):
serializer_class = LoginSerializer
permission_classes = (AllowAny,)
http_method_names = ['post']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
return Response(serializer.validated_data, status=status.HTTP_200_OK)
class RegistrationViewSet(ModelViewSet, TokenObtainPairView):
serializer_class = RegisterSerializer
permission_classes = (AllowAny,)
http_method_names = ['post']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
refresh = RefreshToken.for_user(user)
res = {
"refresh": str(refresh),
"access": str(refresh.access_token),
}
return Response({
"user": serializer.data,
"refresh": res["refresh"],
"token": res["access"]
}, status=status.HTTP_201_CREATED)
class RefreshViewSet(viewsets.ViewSet, TokenRefreshView):
permission_classes = (AllowAny,)
http_method_names = ['post']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
return Response(serializer.validated_data, status=status.HTTP_200_OK)
routers.py
in the core
directory.# core/routers.py
from rest_framework.routers import SimpleRouter
from core.user.viewsets import UserViewSet
from core.auth.viewsets import LoginViewSet, RegistrationViewSet, RefreshViewSet
routes = SimpleRouter()
# AUTHENTICATION
routes.register(r'auth/login', LoginViewSet, basename='auth-login')
routes.register(r'auth/register', RegistrationViewSet, basename='auth-register')
routes.register(r'auth/refresh', RefreshViewSet, basename='auth-refresh')
# USER
routes.register(r'user', UserViewSet, basename='user')
urlpatterns = [
*routes.urls
]
routers.urls
in the standard list of URL patterns in CoreRoot
.# CoreRoot/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('api/', include(('core.routers', 'core'), namespace='core-api')),
]
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
localhost:8000/api/auth/register/
. I'll be using Postman but feel free to use any client.{
"email": "[email protected]",
"password": "12345678",
"username": "testuser"
}
create-react-app
in your machine.yarn create react-app react-auth-app --template typescript
cd react-auth-app
yarn start
django-cors-headers
.pip install django-cors-headers
INSTALLED_APPS
and the middleware.INSTALLED_APPS = [
...
'corsheaders',
...
]
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
settings.py
file.CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000"
]
yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
PostCSS
configuration natively, we also need to install CRACO to be able to configure Tailwind.yarn add @craco/craco
package.json
file. Replace react-
scripts
by craco
."scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
}
tailwindcss
and autoprefixer
as plugins.//craco.config.js
module.exports = {
style: {
postcss: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
},
};
npx tailwindcss-cli@latest init
to generate tailwind.config.js
file containing the minimal configuration for tailwind.module.exports = {
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
index.css
file./*src/index.css*/
@tailwind base;
@tailwind components;
@tailwind utilities;
// ./src/pages/Login.tsx
import React, { useState } from "react";
import * as Yup from "yup";
import { useFormik } from "formik";
import { useDispatch } from "react-redux";
import axios from "axios";
import { useHistory } from "react-router";
function Login() {
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const history = useHistory();
const handleLogin = (email: string, password: string) => {
//
};
const formik = useFormik({
initialValues: {
email: "",
password: "",
},
onSubmit: (values) => {
setLoading(true);
handleLogin(values.email, values.password);
},
validationSchema: Yup.object({
email: Yup.string().trim().required("Le nom d'utilisateur est requis"),
password: Yup.string().trim().required("Le mot de passe est requis"),
}),
});
return (
<div className="h-screen flex bg-gray-bg1">
<div className="w-full max-w-md m-auto bg-white rounded-lg border border-primaryBorder shadow-default py-10 px-16">
<h1 className="text-2xl font-medium text-primary mt-4 mb-12 text-center">
Log in to your account 🔐
</h1>
<form onSubmit={formik.handleSubmit}>
<div className="space-y-4">
<input
className="border-b border-gray-300 w-full px-2 h-8 rounded focus:border-blue-500"
id="email"
type="email"
placeholder="Email"
name="email"
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.errors.email ? <div>{formik.errors.email} </div> : null}
<input
className="border-b border-gray-300 w-full px-2 h-8 rounded focus:border-blue-500"
id="password"
type="password"
placeholder="Password"
name="password"
value={formik.values.password}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.errors.password ? (
<div>{formik.errors.password} </div>
) : null}
</div>
<div className="text-danger text-center my-2" hidden={false}>
{message}
</div>
<div className="flex justify-center items-center mt-6">
<button
type="submit"
disabled={loading}
className="rounded border-gray-300 p-2 w-32 bg-blue-700 text-white"
>
Login
</button>
</div>
</form>
</div>
</div>
);
}
export default Login;
// ./src/pages/Profile.tsx
import React from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
const Profile = () => {
const dispatch = useDispatch();
const history = useHistory();
const handleLogout = () => {
//
};
return (
<div className="w-full h-screen">
<div className="w-full p-6">
<button
onClick={handleLogout}
className="rounded p-2 w-32 bg-red-700 text-white"
>
Deconnexion
</button>
</div>
<div className="w-full h-full text-center items-center">
<p className="self-center my-auto">Welcome</p>
</div>
</div>
);
};
export default Profile;
.env
file at the root of the project and put this here../.env
REACT_APP_API_URL=localhost:8000/api
redux-toolkit
to save, account state, and tokens when the user signs in. We'll also write an action for logout.yarn add @reduxjs/toolkit redux react-redux redux-persist
store
in src
.slices
and create in this directory a file named auth.ts
.// ./src/types.ts
export interface AccountResponse {
user: {
id: string;
email: string;
username: string;
is_active: boolean;
created: Date;
updated: Date;
};
access: string;
refresh: string;
}
authSlice
.// ./src/store/slices/auth.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AccountResponse } from "../../types";
type State = {
token: string | null;
refreshToken: string | null;
account: AccountResponse | null;
};
const initialState: State = { token: null, refreshToken: null, account: null };
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
setAuthTokens(
state: State,
action: PayloadAction<{ token: string; refreshToken: string }>
) {
state.refreshToken = action.payload.refreshToken;
state.token = action.payload.token;
},
setAccount(state: State, action: PayloadAction<AccountResponse>) {
state.account = action.payload;
},
logout(state: State) {
state.account = null;
state.refreshToken = null;
state.token = null;
},
},
});
export default authSlice;
index.ts
. And add the following content.// ./src/store/index.ts
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import { combineReducers } from "redux";
import {
FLUSH,
PAUSE,
PERSIST,
persistReducer,
persistStore,
PURGE,
REGISTER,
REHYDRATE,
} from "redux-persist";
import storage from "redux-persist/lib/storage";
import authSlice from "./slices/auth";
const rootReducer = combineReducers({
auth: authSlice.reducer,
});
const persistedReducer = persistReducer(
{
key: "root",
version: 1,
storage: storage,
},
rootReducer
);
const store = configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
});
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof rootReducer>;
export default store;
store
accessible for all components by wrapping <App />
(top-level-component) in :// ./src/App.tsx
import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { Login, Profile } from "./pages";
import store, { persistor } from "./store";
import { PersistGate } from "redux-persist/integration/react";
import { Provider } from "react-redux";
import ProtectedRoute from "./routes/ProtectedRoute";
export default function App() {
return (
<Provider store={store}>
<PersistGate persistor={persistor} loading={null}>
<Router>
<div>
<Switch>
<Route exact path="/login" component={Login} />
<ProtectedRoute exact path="/" component={Profile} />
</Switch>
</div>
</Router>
</PersistGate>
</Provider>
);
}
<ProtectedRoute />
component to help us hide pages that require sessions from the other ones.<ProtectedRoute />
component using React Router. cd src & mkdir routes
cd routes
ProtectedRoute.tsx
, and write this :// ./src/routes/ProtectedRoute.tsx
import React from "react";
import { Redirect, Route, RouteProps } from "react-router";
import { useSelector } from "react-redux";
import { RootState } from "../store";
const ProtectedRoute = (props: RouteProps) => {
const auth = useSelector((state: RootState) => state.auth);
if (auth.account) {
if (props.path === "/login") {
return <Redirect to={"/"} />;
}
return <Route {...props} />;
} else if (!auth.account) {
return <Redirect to={"/login"} />;
} else {
return <div>Not found</div>;
}
};
export default ProtectedRoute;
auth
. Actually, every time a user successfully signs in, we'll use the slices to persist the account state and the tokens in the storage.return <Route {...props} />;
or he is directly redirected to the login page return <Redirect to={"/login"} />;
.// ./src/pages/Login.tsx
import authSlice from "../store/slices/auth";
...
const handleLogin = (email: string, password: string) => {
axios
.post(`${process.env.REACT_APP_API_URL}/auth/login/`, { email, password })
.then((res) => {
dispatch(
authSlice.actions.setAuthTokens({
token: res.data.access,
refreshToken: res.data.refresh,
})
);
dispatch(authSlice.actions.setAccount(res.data.user));
setLoading(false);
history.push("/");
})
.catch((err) => {
setMessage(err.response.data.detail.toString());
});
};
...
// ./src/pages/Profile.tsx
import authSlice from "../store/slices/auth";
...
const handleLogout = () => {
dispatch(authSlice.actions.logout());
history.push("/login");
};
...
axios
and axios-auth-refresh
.
Here's how it'll work: yarn add axios-auth-refresh
utils
, and inside this directory, create a file named axios.ts
. It will contain the code of our fetcher.import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import store from '../store';
import authSlice from '../store/slices/auth';
const axiosService = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
axiosService.interceptors.request.use(async (config) => {
const { token } = store.getState().auth;
if (token !== null) {
config.headers.Authorization = 'Bearer ' + token;
// @ts-ignore
console.debug('[Request]', config.baseURL + config.url, JSON.stringify(token));
}
return config;
});
axiosService.interceptors.response.use(
(res) => {
// @ts-ignore
console.debug('[Response]', res.config.baseURL + res.config.url, res.status, res.data);
return Promise.resolve(res);
},
(err) => {
console.debug(
'[Response]',
err.config.baseURL + err.config.url,
err.response.status,
err.response.data
);
return Promise.reject(err);
}
);
// @ts-ignore
const refreshAuthLogic = async (failedRequest) => {
const { refreshToken } = store.getState().auth;
if (refreshToken !== null) {
return axios
.post(
'/auth/refresh/',
{
refresh: refreshToken,
},
{
baseURL: process.env.REACT_APP_API_URL
}
)
.then((resp) => {
const { access, refresh } = resp.data;
failedRequest.response.config.headers.Authorization = 'Bearer ' + access;
store.dispatch(
authSlice.actions.setAuthTokens({ token: access, refreshToken: refresh })
);
})
.catch((err) => {
if (err.response && err.response.status === 401) {
store.dispatch(authSlice.actions.setLogout());
}
});
}
};
createAuthRefreshInterceptor(axiosService, refreshAuthLogic);
export function fetcher<T = any>(url: string) {
return axiosService.get<T>(url).then((res) => res.data);
}
export default axiosService;
import React from "react";
import {useDispatch, useSelector} from "react-redux";
import {useHistory, useLocation} from "react-router";
import authSlice from "../store/slices/auth";
import useSWR from 'swr';
import {fetcher} from "../utils/axios";
import {UserResponse} from "../utils/types";
import {RootState} from "../store";
interface LocationState {
userId: string;
}
const Profile = () => {
const account = useSelector((state: RootState) => state.auth.account);
const dispatch = useDispatch();
const history = useHistory();
const userId = account?.id;
const user = useSWR<UserResponse>(`/user/${userId}/`, fetcher)
const handleLogout = () => {
dispatch(authSlice.actions.setLogout());
history.push("/login");
};
return (
<div className="w-full h-screen">
<div className="w-full p-6">
<button
onClick={handleLogout}
className="rounded p-2 w-32 bg-red-700 text-white"
>
Deconnexion
</button>
</div>
{
user.data ?
<div className="w-full h-full text-center items-center">
<p className="self-center my-auto">Welcome, {user.data?.username}</p>
</div>
:
<p className="text-center items-center">Loading ...</p>
}
</div>
);
};
export default Profile;