├── .prettierrc.json
├── .env.example
├── .prettierignore
├── src
├── react-app-env.d.ts
├── index.css
├── pages
│ ├── index.ts
│ ├── Profile.tsx
│ └── Login.tsx
├── utils
│ ├── types.ts
│ └── axios.ts
├── types.ts
├── index.tsx
├── routes
│ └── ProtectedRoute.tsx
├── App.css
├── App.tsx
├── store
│ ├── slices
│ │ └── auth.ts
│ └── index.ts
└── logo.svg
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── craco.config.js
├── tailwind.config.js
├── README.md
├── .gitignore
├── tsconfig.json
└── package.json
/.prettierrc.json:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=http://localhost:8000/api/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koladev32/django-react-auth-app/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koladev32/django-react-auth-app/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koladev32/django-react-auth-app/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | import Login from "./Login";
2 | import Profile from "./Profile";
3 |
4 | export { Login, Profile };
5 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | // craco.config.js
2 | module.exports = {
3 | style: {
4 | postcss: {
5 | plugins: [require("tailwindcss"), require("autoprefixer")],
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | export interface UserResponse {
2 | email: string;
3 | username: string;
4 | is_active: string;
5 | created: Date;
6 | updated: Date;
7 | id: string;
8 | }
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface AccountResponse {
2 | user: {
3 | id: string;
4 | email: string;
5 | username: string;
6 | is_active: boolean;
7 | created: Date;
8 | updated: Date;
9 | };
10 | access: string;
11 | refresh: string;
12 | }
13 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | );
12 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {},
6 | },
7 | variants: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | };
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Available Scripts
2 |
3 | In the project directory, you can run:
4 |
5 | ```shell
6 | yarn install
7 | yarn start
8 | ```
9 | And hit `http://localhost:3000/`
10 |
11 | If you are connecting to a backend, create a `.env` file following the structure in `.env.example`.
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | .env
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/routes/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Redirect, Route, RouteProps } from "react-router";
3 | import { useSelector } from "react-redux";
4 | import { RootState } from "../store";
5 |
6 | const ProtectedRoute = (props: RouteProps) => {
7 | const auth = useSelector((state: RootState) => state.auth);
8 |
9 | if (auth.account) {
10 | if (props.path === "/login") {
11 | return ;
12 | }
13 | return ;
14 | } else if (!auth.account) {
15 | return ;
16 | } else {
17 | return
Not found
;
18 | }
19 | };
20 |
21 | export default ProtectedRoute;
22 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
3 | import { Login, Profile } from "./pages";
4 | import store, { persistor } from "./store";
5 | import { PersistGate } from "redux-persist/integration/react";
6 | import { Provider } from "react-redux";
7 | import ProtectedRoute from "./routes/ProtectedRoute";
8 |
9 | export default function App() {
10 | return (
11 |
12 |
13 |
14 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/store/slices/auth.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 | import { AccountResponse } from "../../types";
3 |
4 | type State = {
5 | token: string | null;
6 | refreshToken: string | null;
7 | account: AccountResponse | null;
8 | };
9 |
10 | const initialState: State = { token: null, refreshToken: null, account: null };
11 |
12 | const authSlice = createSlice({
13 | name: "auth",
14 | initialState,
15 | reducers: {
16 | setAuthTokens(
17 | state: State,
18 | action: PayloadAction<{ token: string; refreshToken: string }>
19 | ) {
20 | state.refreshToken = action.payload.refreshToken;
21 | state.token = action.payload.token;
22 | },
23 | setAccount(state: State, action: PayloadAction) {
24 | state.account = action.payload;
25 | },
26 | setLogout(state: State) {
27 | state.account = null;
28 | state.refreshToken = null;
29 | state.token = null;
30 | },
31 | },
32 | });
33 |
34 | export default authSlice;
35 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
2 | import { combineReducers } from "redux";
3 | import {
4 | FLUSH,
5 | PAUSE,
6 | PERSIST,
7 | persistReducer,
8 | persistStore,
9 | PURGE,
10 | REGISTER,
11 | REHYDRATE,
12 | } from "redux-persist";
13 | import storage from "redux-persist/lib/storage";
14 | import authSlice from "./slices/auth";
15 |
16 | const rootReducer = combineReducers({
17 | auth: authSlice.reducer,
18 | });
19 |
20 | const persistedReducer = persistReducer(
21 | {
22 | key: "root",
23 | version: 1,
24 | storage: storage,
25 | },
26 | rootReducer
27 | );
28 |
29 | const store = configureStore({
30 | reducer: persistedReducer,
31 | middleware: getDefaultMiddleware({
32 | serializableCheck: {
33 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
34 | },
35 | }),
36 | });
37 |
38 | export const persistor = persistStore(store);
39 | export type RootState = ReturnType;
40 |
41 | export default store;
42 |
--------------------------------------------------------------------------------
/src/pages/Profile.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {useDispatch, useSelector} from "react-redux";
3 | import {useHistory, useLocation} from "react-router";
4 | import authSlice from "../store/slices/auth";
5 | import useSWR from 'swr';
6 | import {fetcher} from "../utils/axios";
7 | import {UserResponse} from "../utils/types";
8 | import {RootState} from "../store";
9 |
10 | interface LocationState {
11 | userId: string;
12 | }
13 |
14 |
15 | const Profile = () => {
16 | const account = useSelector((state: RootState) => state.auth.account);
17 | const dispatch = useDispatch();
18 | const history = useHistory();
19 | // @ts-ignore
20 | const userId = account?.id;
21 |
22 | const user = useSWR(`/user/${userId}/`, fetcher)
23 |
24 | const handleLogout = () => {
25 | dispatch(authSlice.actions.setLogout());
26 | history.push("/login");
27 | };
28 | return (
29 |
30 |
31 |
37 |
38 | {
39 | user.data ?
40 |
41 |
Welcome, {user.data?.username}
42 |
43 | :
44 |
Loading ...
45 | }
46 |
47 | );
48 | };
49 |
50 | export default Profile;
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "django-react-auth-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@craco/craco": "^6.1.2",
7 | "@reduxjs/toolkit": "^1.6.0",
8 | "@testing-library/jest-dom": "^5.11.4",
9 | "@testing-library/react": "^11.1.0",
10 | "@testing-library/user-event": "^12.1.10",
11 | "@types/axios": "^0.14.0",
12 | "@types/jest": "^26.0.15",
13 | "@types/node": "^12.0.0",
14 | "@types/react": "^17.0.0",
15 | "@types/react-dom": "^17.0.0",
16 | "@types/react-redux": "^7.1.16",
17 | "@types/react-router-dom": "^5.1.7",
18 | "@types/redux-persist": "^4.3.1",
19 | "@types/yup": "^0.29.11",
20 | "axios": "^0.21.1",
21 | "axios-auth-refresh": "^3.2.1",
22 | "formik": "^2.2.9",
23 | "react": "^17.0.2",
24 | "react-dom": "^17.0.2",
25 | "react-redux": "^7.2.4",
26 | "react-router-dom": "^5.2.0",
27 | "react-scripts": "4.0.3",
28 | "redux": "^4.1.0",
29 | "redux-persist": "^6.0.0",
30 | "swr": "^1.0.1",
31 | "typescript": "^4.1.2",
32 | "web-vitals": "^1.0.1",
33 | "yup": "^0.32.9"
34 | },
35 | "scripts": {
36 | "start": "craco start",
37 | "build": "craco build",
38 | "test": "craco test",
39 | "eject": "react-scripts eject"
40 | },
41 | "eslintConfig": {
42 | "extends": [
43 | "react-app",
44 | "react-app/jest"
45 | ]
46 | },
47 | "browserslist": {
48 | "production": [
49 | ">0.2%",
50 | "not dead",
51 | "not op_mini all"
52 | ],
53 | "development": [
54 | "last 1 chrome version",
55 | "last 1 firefox version",
56 | "last 1 safari version"
57 | ]
58 | },
59 | "devDependencies": {
60 | "autoprefixer": "9",
61 | "postcss": "7",
62 | "prettier": "2.3.1",
63 | "tailwindcss": "npm:@tailwindcss/postcss7-compat"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import createAuthRefreshInterceptor from 'axios-auth-refresh';
3 | import store from '../store';
4 | import authSlice from '../store/slices/auth';
5 |
6 | const axiosService = axios.create({
7 | baseURL: process.env.REACT_APP_API_URL,
8 | headers: {
9 | 'Content-Type': 'application/json',
10 | },
11 | });
12 |
13 | axiosService.interceptors.request.use(async (config) => {
14 | const { token } = store.getState().auth;
15 |
16 | if (token !== null) {
17 | config.headers.Authorization = 'Bearer ' + token;
18 | // @ts-ignore
19 | console.debug('[Request]', config.baseURL + config.url, JSON.stringify(token));
20 | }
21 | return config;
22 | });
23 |
24 | axiosService.interceptors.response.use(
25 | (res) => {
26 | // @ts-ignore
27 | console.debug('[Response]', res.config.baseURL + res.config.url, res.status, res.data);
28 | return Promise.resolve(res);
29 | },
30 | (err) => {
31 | console.debug(
32 | '[Response]',
33 | err.config.baseURL + err.config.url,
34 | err.response.status,
35 | err.response.data
36 | );
37 | return Promise.reject(err);
38 | }
39 | );
40 |
41 | // @ts-ignore
42 | const refreshAuthLogic = async (failedRequest) => {
43 | const { refreshToken } = store.getState().auth;
44 | if (refreshToken !== null) {
45 | return axios
46 | .post(
47 | '/auth/refresh/',
48 | {
49 | refresh: refreshToken,
50 | },
51 | {
52 | baseURL: process.env.REACT_APP_API_URL
53 | }
54 | )
55 | .then((resp) => {
56 | const { access, refresh } = resp.data;
57 | failedRequest.response.config.headers.Authorization = 'Bearer ' + access;
58 | store.dispatch(
59 | authSlice.actions.setAuthTokens({ token: access, refreshToken: refresh })
60 | );
61 | })
62 | .catch((err) => {
63 | if (err.response && err.response.status === 401) {
64 | store.dispatch(authSlice.actions.setLogout());
65 | }
66 | });
67 | }
68 | };
69 |
70 | createAuthRefreshInterceptor(axiosService, refreshAuthLogic);
71 |
72 | export function fetcher(url: string) {
73 | return axiosService.get(url).then((res) => res.data);
74 | }
75 |
76 | export default axiosService;
77 |
--------------------------------------------------------------------------------
/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import * as Yup from "yup";
3 | import { useFormik } from "formik";
4 | import { useDispatch } from "react-redux";
5 | import authSlice from "../store/slices/auth";
6 | import axios from "axios";
7 | import { useHistory } from "react-router";
8 |
9 | function Login() {
10 | const [message, setMessage] = useState("");
11 | const [loading, setLoading] = useState(false);
12 | const dispatch = useDispatch();
13 | const history = useHistory();
14 |
15 | const handleLogin = (email: string, password: string) => {
16 | axios
17 | .post(`${process.env.REACT_APP_API_URL}/auth/login/`, { email, password })
18 | .then((res) => {
19 | dispatch(
20 | authSlice.actions.setAuthTokens({
21 | token: res.data.access,
22 | refreshToken: res.data.refresh,
23 | })
24 | );
25 | dispatch(authSlice.actions.setAccount(res.data.user));
26 | setLoading(false);
27 | history.push("/", {
28 | userId: res.data.id
29 | });
30 | })
31 | .catch((err) => {
32 | setMessage(err.response.data.detail.toString());
33 | });
34 | };
35 |
36 | const formik = useFormik({
37 | initialValues: {
38 | email: "",
39 | password: "",
40 | },
41 | onSubmit: (values) => {
42 | setLoading(true);
43 | handleLogin(values.email, values.password);
44 | },
45 | validationSchema: Yup.object({
46 | email: Yup.string().trim().required("Le nom d'utilisateur est requis"),
47 | password: Yup.string().trim().required("Le mot de passe est requis"),
48 | }),
49 | });
50 |
51 | return (
52 |
53 |
54 |
55 | Log in to your account 🔐
56 |
57 |
98 |
99 |
100 | );
101 | }
102 |
103 | export default Login;
104 |
--------------------------------------------------------------------------------