├── backend
├── core
│ ├── __init__.py
│ ├── asgi.py
│ ├── wsgi.py
│ ├── urls.py
│ └── settings.py
├── authentication
│ ├── __init__.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ ├── admin.py
│ ├── apps.py
│ ├── views.py
│ └── urls.py
├── requirements.txt
├── manage.py
└── .gitignore
├── frontend
├── public
│ └── favicon.ico
├── next.config.js
├── .eslintrc.json
├── pages
│ ├── _document.tsx
│ ├── _app.tsx
│ ├── index.tsx
│ ├── profile.tsx
│ └── api
│ │ └── auth
│ │ └── [...nextauth].js
├── types
│ └── next-auth.d.ts
├── tsconfig.json
├── package.json
└── .gitignore
└── README.md
/backend/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/authentication/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/authentication/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/authentication/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/backend/authentication/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/authentication/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/backend/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duplxey/django-rest-authjs/HEAD/backend/requirements.txt
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duplxey/django-rest-authjs/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | };
5 |
6 | module.exports = nextConfig;
7 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "quotes": ["error", "double"],
5 | "semi": ["error", "always"],
6 | "indent": ["error", 2]
7 | }
8 | }
--------------------------------------------------------------------------------
/backend/authentication/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AuthenticationConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "authentication"
7 |
--------------------------------------------------------------------------------
/backend/authentication/views.py:
--------------------------------------------------------------------------------
1 | from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
2 | from allauth.socialaccount.providers.oauth2.client import OAuth2Client
3 | from dj_rest_auth.registration.views import SocialLoginView
4 |
5 |
6 | class GoogleLogin(SocialLoginView):
7 | adapter_class = GoogleOAuth2Adapter
8 | callback_url = "http://localhost:3000/api/auth/callback/google"
9 | client_class = OAuth2Client
10 |
--------------------------------------------------------------------------------
/frontend/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import {Head, Html, Main, NextScript} from "next/document";
2 | import {ColorModeScript} from "@chakra-ui/react";
3 | import {theme} from "./_app";
4 |
5 | export default function Document() {
6 | return (
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/backend/core/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for core project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/backend/core/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for core project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/frontend/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type {AppProps} from "next/app";
2 | import {SessionProvider} from "next-auth/react";
3 | import {ChakraProvider, extendTheme} from "@chakra-ui/react";
4 |
5 | export const theme = extendTheme({});
6 |
7 | export default function App({Component, pageProps: {session, ...pageProps}}: AppProps) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 |
3 | // prevents IDEs from removing the unused `NextAuth` import
4 | NextAuth.name;
5 |
6 | declare module "next-auth" {
7 | /**
8 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context.
9 | * Session `user` should only contain immutable variables.
10 | */
11 | interface Session {
12 | access_token: string;
13 | refresh_token: string;
14 | user: {
15 | pk: number;
16 | username: string;
17 | email: string | null;
18 | },
19 | }
20 | }
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/*": ["./*"]
19 | },
20 | "useUnknownInCatchVariables": false,
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@chakra-ui/react": "^2.5.5",
13 | "@emotion/react": "^11.10.6",
14 | "@emotion/styled": "^11.10.6",
15 | "@types/node": "18.16.0",
16 | "@types/react": "18.0.38",
17 | "@types/react-dom": "18.0.11",
18 | "axios": "^1.3.6",
19 | "eslint": "8.39.0",
20 | "eslint-config-next": "13.3.1",
21 | "framer-motion": "^10.12.4",
22 | "next": "13.3.1",
23 | "next-auth": "^4.22.1",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "typescript": "5.0.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import {useRouter} from "next/router";
2 | import {signIn, useSession} from "next-auth/react";
3 | import {Box, Button, Spinner, Text, VStack} from "@chakra-ui/react";
4 |
5 | export default function Home() {
6 |
7 | const router = useRouter();
8 | const {data: session, status} = useSession();
9 |
10 | if (status == "loading") {
11 | return ;
12 | }
13 |
14 | // If the user is authenticated redirect to `/profile`
15 | if (session) {
16 | router.push("profile");
17 | return;
18 | }
19 |
20 | return (
21 |
22 |
23 | You are not authenticated.
24 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/backend/authentication/urls.py:
--------------------------------------------------------------------------------
1 | from dj_rest_auth.jwt_auth import get_refresh_view
2 | from dj_rest_auth.registration.views import RegisterView
3 | from dj_rest_auth.views import LoginView, LogoutView, UserDetailsView
4 | from django.urls import path
5 | from rest_framework_simplejwt.views import TokenVerifyView
6 |
7 | from authentication.views import GoogleLogin
8 |
9 | urlpatterns = [
10 | path("register/", RegisterView.as_view(), name="rest_register"),
11 | path("login/", LoginView.as_view(), name="rest_login"),
12 | path("logout/", LogoutView.as_view(), name="rest_logout"),
13 | path("user/", UserDetailsView.as_view(), name="rest_user_details"),
14 | path("google/", GoogleLogin.as_view(), name="google_login"),
15 | path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
16 | path("token/refresh/", get_refresh_view().as_view(), name="token_refresh"),
17 | ]
18 |
--------------------------------------------------------------------------------
/backend/core/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | URL configuration for core project.
3 |
4 | The `urlpatterns` list routes URLs to views. For more information please see:
5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/
6 | Examples:
7 | Function views
8 | 1. Add an import: from my_app import views
9 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
10 | Class-based views
11 | 1. Add an import: from other_app.views import Home
12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13 | Including another URLconf
14 | 1. Import the include() function: from django.urls import include, path
15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16 | """
17 | from django.contrib import admin
18 | from django.urls import path, include
19 |
20 | urlpatterns = [
21 | path("api/auth/", include("authentication.urls")),
22 | path("admin/", admin.site.urls),
23 | ]
24 |
--------------------------------------------------------------------------------
/frontend/pages/profile.tsx:
--------------------------------------------------------------------------------
1 | import {useState} from "react";
2 | import {signOut, useSession} from "next-auth/react";
3 | import {Box, Button, Code, HStack, Spinner, Text, VStack} from "@chakra-ui/react";
4 | import axios from "axios";
5 |
6 | export default function Home() {
7 |
8 | const {data: session, status} = useSession({required: true});
9 | const [response, setResponse] = useState("{}");
10 |
11 | const getUserDetails = async (useToken: boolean) => {
12 | try {
13 | const response = await axios({
14 | method: "get",
15 | url: process.env.NEXT_PUBLIC_BACKEND_URL + "auth/user/",
16 | headers: useToken ? {Authorization: "Bearer " + session?.access_token} : {},
17 | });
18 | setResponse(JSON.stringify(response.data));
19 | } catch (error) {
20 | setResponse(error.message);
21 | }
22 | };
23 |
24 | if (status == "loading") {
25 | return ;
26 | }
27 |
28 | if (session) {
29 | return (
30 |
31 |
32 | PK: {session.user.pk}
33 | Username: {session.user.username}
34 | Email: {session.user.email || "Not provided"}
35 |
36 | {response}
37 |
38 |
39 |
40 |
43 |
46 |
49 |
50 |
51 | );
52 | }
53 |
54 | return <>>;
55 | }
56 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/webstorm+all,nextjs
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=webstorm+all,nextjs
3 |
4 | .idea/
5 |
6 | ### NextJS ###
7 | # dependencies
8 | /node_modules
9 | /.pnp
10 | .pnp.js
11 |
12 | # testing
13 | /coverage
14 |
15 | # next.js
16 | /.next/
17 | /out/
18 |
19 | # production
20 | /build
21 |
22 | # misc
23 | .DS_Store
24 | *.pem
25 |
26 | # debug
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 | .pnpm-debug.log*
31 |
32 | # local env files
33 | .env*.local
34 |
35 | # vercel
36 | .vercel
37 |
38 | # typescript
39 | *.tsbuildinfo
40 | next-env.d.ts
41 |
42 | ### WebStorm+all ###
43 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
44 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
45 |
46 | # User-specific stuff
47 | .idea/**/workspace.xml
48 | .idea/**/tasks.xml
49 | .idea/**/usage.statistics.xml
50 | .idea/**/dictionaries
51 | .idea/**/shelf
52 |
53 | # AWS User-specific
54 | .idea/**/aws.xml
55 |
56 | # Generated files
57 | .idea/**/contentModel.xml
58 |
59 | # Sensitive or high-churn files
60 | .idea/**/dataSources/
61 | .idea/**/dataSources.ids
62 | .idea/**/dataSources.local.xml
63 | .idea/**/sqlDataSources.xml
64 | .idea/**/dynamic.xml
65 | .idea/**/uiDesigner.xml
66 | .idea/**/dbnavigator.xml
67 |
68 | # Gradle
69 | .idea/**/gradle.xml
70 | .idea/**/libraries
71 |
72 | # Gradle and Maven with auto-import
73 | # When using Gradle or Maven with auto-import, you should exclude module files,
74 | # since they will be recreated, and may cause churn. Uncomment if using
75 | # auto-import.
76 | # .idea/artifacts
77 | # .idea/compiler.xml
78 | # .idea/jarRepositories.xml
79 | # .idea/modules.xml
80 | # .idea/*.iml
81 | # .idea/modules
82 | # *.iml
83 | # *.ipr
84 |
85 | # CMake
86 | cmake-build-*/
87 |
88 | # Mongo Explorer plugin
89 | .idea/**/mongoSettings.xml
90 |
91 | # File-based project format
92 | *.iws
93 |
94 | # IntelliJ
95 | out/
96 |
97 | # mpeltonen/sbt-idea plugin
98 | .idea_modules/
99 |
100 | # JIRA plugin
101 | atlassian-ide-plugin.xml
102 |
103 | # Cursive Clojure plugin
104 | .idea/replstate.xml
105 |
106 | # SonarLint plugin
107 | .idea/sonarlint/
108 |
109 | # Crashlytics plugin (for Android Studio and IntelliJ)
110 | com_crashlytics_export_strings.xml
111 | crashlytics.properties
112 | crashlytics-build.properties
113 | fabric.properties
114 |
115 | # Editor-based Rest Client
116 | .idea/httpRequests
117 |
118 | # Android studio 3.1+ serialized cache file
119 | .idea/caches/build_file_checksums.ser
120 |
121 | ### WebStorm+all Patch ###
122 | # Ignore everything but code style settings and run configurations
123 | # that are supposed to be shared within teams.
124 |
125 | .idea/*
126 |
127 | !.idea/codeStyles
128 | !.idea/runConfigurations
129 |
130 | # End of https://www.toptal.com/developers/gitignore/api/webstorm+all,nextjs
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django REST-based Authentication with Auth.js
2 |
3 | The repository is split into two directories:
4 |
5 | 1. `backend` -- the backend part of the project (Django, DRF, dj-rest-auth)
6 | 2. `frontend` -- the frontend part of the project (Next.js, Auth.js)
7 |
8 | > If you're interested only in Django REST framework authentication take a look at [this repo](https://github.com/duplxey/django-rest-allauth).
9 |
10 | ## Want to learn how to build this?
11 |
12 | Check out the [post](https://testdriven.io/blog/django-rest-authjs/).
13 |
14 | ## Want to use this project?
15 |
16 | Go ahead and fork/clone the repository and then setup backend and frontend individually.
17 |
18 | ### Backend
19 |
20 | 1. Change directory to `backend`.
21 |
22 | 1. Create and activate a virtual environment:
23 |
24 | ```sh
25 | $ python3 -m venv venv && source venv/bin/activate
26 | ```
27 |
28 | 1. Install the requirements:
29 |
30 | ```sh
31 | (venv)$ pip install -r requirements.txt
32 | ```
33 |
34 | 1. Apply the migrations:
35 |
36 | ```sh
37 | (venv)$ python manage.py migrate
38 | ```
39 |
40 | 1. Register your app with social providers and take note of your client IDs and secrets.
41 |
42 | 1. Enter the client IDs and secrets in *core/settings.py* respectively:
43 |
44 | ```python
45 | SOCIALACCOUNT_PROVIDERS = {
46 | "google": {
47 | "APP": {
48 | "client_id": "",
49 | "secret": "",
50 | "key": "", # leave empty
51 | },
52 | "SCOPE": [
53 | "profile",
54 | "email",
55 | ],
56 | "AUTH_PARAMS": {
57 | "access_type": "online",
58 | },
59 | "VERIFIED_EMAIL": True,
60 | }
61 | }
62 | ```
63 |
64 | 1. Run the development server:
65 |
66 | ```sh
67 | (venv)$ python manage.py runserver
68 | ```
69 |
70 | 1. Your authentication API is now accessible at [http://localhost:8000/api/auth/](http://localhost:8000/api/auth/).
71 |
72 | ### Frontend
73 |
74 | 1. Change directory to `frontend`.
75 |
76 | 1. Install the dependencies:
77 |
78 | ```sh
79 | $ npm install
80 | ```
81 |
82 | 1. Create an *.env.local* file in the project root with the following contents:
83 |
84 | ```env
85 | NEXTAUTH_URL=http://127.0.0.1:3000
86 | NEXTAUTH_SECRET=
87 | NEXTAUTH_BACKEND_URL=http://127.0.0.1:8000/api/
88 | NEXT_PUBLIC_BACKEND_URL=http://127.0.0.1:8000/api/
89 |
90 | GOOGLE_CLIENT_ID=
91 | GOOGLE_CLIENT_SECRET=
92 | ```
93 |
94 | 1. Run the development server:
95 |
96 | ```sh
97 | $ next dev
98 | ```
99 |
100 | 1. Navigate to [http://localhost:3000/](http://localhost:3000/) in your favorite web browser.
101 |
--------------------------------------------------------------------------------
/frontend/pages/api/auth/[...nextauth].js:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import CredentialsProvider from "next-auth/providers/credentials";
3 | import GoogleProvider from "next-auth/providers/google";
4 | import axios from "axios";
5 |
6 | // These two values should be a bit less than actual token lifetimes
7 | const BACKEND_ACCESS_TOKEN_LIFETIME = 45 * 60; // 45 minutes
8 | const BACKEND_REFRESH_TOKEN_LIFETIME = 6 * 24 * 60 * 60; // 6 days
9 |
10 | const getCurrentEpochTime = () => {
11 | return Math.floor(new Date().getTime() / 1000);
12 | };
13 |
14 | const SIGN_IN_HANDLERS = {
15 | "credentials": async (user, account, profile, email, credentials) => {
16 | // Authentication is already performed in `CredentialsProvider.authorize()` function
17 | return true;
18 | },
19 | "google": async (user, account, profile, email, credentials) => {
20 | try {
21 | const response = await axios({
22 | method: "post",
23 | url: process.env.NEXTAUTH_BACKEND_URL + "auth/google/",
24 | data: {
25 | access_token: account["id_token"]
26 | },
27 | });
28 | account["meta"] = response.data;
29 | return true;
30 | } catch (error) {
31 | console.error(error);
32 | return false;
33 | }
34 | }
35 | };
36 | const SIGN_IN_PROVIDERS = Object.keys(SIGN_IN_HANDLERS);
37 |
38 | export const authOptions = {
39 | secret: process.env.AUTH_SECRET,
40 | session: {
41 | strategy: "jwt",
42 | maxAge: BACKEND_REFRESH_TOKEN_LIFETIME,
43 | },
44 | providers: [
45 | CredentialsProvider({
46 | name: "Credentials",
47 | credentials: {
48 | username: {label: "Username", type: "text"},
49 | password: {label: "Password", type: "password"}
50 | },
51 | // The data returned from this function is passed forward as the
52 | // `user` variable to the signIn() and jwt() callback
53 | async authorize(credentials, req) {
54 | try {
55 | const response = await axios({
56 | url: process.env.NEXTAUTH_BACKEND_URL + "auth/login/",
57 | method: "post",
58 | data: credentials,
59 | });
60 | const data = response.data;
61 | if (data) return data;
62 | } catch (error) {
63 | console.error(error);
64 | }
65 | return null;
66 | },
67 | }),
68 | GoogleProvider({
69 | clientId: process.env.GOOGLE_CLIENT_ID,
70 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
71 | authorization: {
72 | params: {
73 | prompt: "consent",
74 | access_type: "offline",
75 | response_type: "code"
76 | }
77 | }
78 | }),
79 | ],
80 | callbacks: {
81 | async signIn({user, account, profile, email, credentials}) {
82 | if (!SIGN_IN_PROVIDERS.includes(account.provider)) return false;
83 | return SIGN_IN_HANDLERS[account.provider](user, account, profile, email, credentials);
84 | },
85 | async jwt({user, token, account}) {
86 | // If `user` and `account` are set that means it is a login/sign in event
87 | if (user && account) {
88 | let backendResponse = account.provider === "credentials" ? user : account.meta;
89 | token["user"] = backendResponse.user;
90 | token["access_token"] = backendResponse.access;
91 | token["refresh_token"] = backendResponse.refresh;
92 | token["ref"] = getCurrentEpochTime() + BACKEND_ACCESS_TOKEN_LIFETIME;
93 | return token;
94 | }
95 | // Refresh the backend token if necessary
96 | if (getCurrentEpochTime() > token["ref"]) {
97 | const response = await axios({
98 | method: "post",
99 | url: process.env.NEXTAUTH_BACKEND_URL + "auth/token/refresh/",
100 | data: {
101 | refresh: token["refresh_token"],
102 | },
103 | });
104 | token["access_token"] = response.data.access;
105 | token["refresh_token"] = response.data.refresh;
106 | token["ref"] = getCurrentEpochTime() + BACKEND_ACCESS_TOKEN_LIFETIME;
107 | }
108 | // TODO: Implement Google refresh token rotation
109 | // This can be implemented in a similar way as the backend token rotation.
110 | // For reference check out: https://authjs.dev/guides/basics/refresh-token-rotation
111 | return token;
112 | },
113 | // We're using JWT instead of database, so we are forced to pass
114 | // backend's `access_token` and `refresh_token` to the client
115 | async session({token}) {
116 | return token;
117 | },
118 | }
119 | };
120 |
121 | export default NextAuth(authOptions);
--------------------------------------------------------------------------------
/backend/core/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for core project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.2/ref/settings/
11 | """
12 | from datetime import timedelta
13 | from pathlib import Path
14 |
15 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
16 | BASE_DIR = Path(__file__).resolve().parent.parent
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = "django-insecure-z6dh*i8cjajq$o6lg-@$%3v06vpl!irr9+v0=+d&5d$f#-(t"
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | "django.contrib.admin",
35 | "django.contrib.auth",
36 | "django.contrib.contenttypes",
37 | "django.contrib.sessions",
38 | "django.contrib.messages",
39 | "django.contrib.staticfiles",
40 | "django.contrib.sites",
41 | # 3rd party
42 | "corsheaders",
43 | "rest_framework",
44 | "rest_framework.authtoken",
45 | "rest_framework_simplejwt",
46 | "allauth",
47 | "allauth.account",
48 | "allauth.socialaccount",
49 | "allauth.socialaccount.providers.google",
50 | "dj_rest_auth",
51 | "dj_rest_auth.registration",
52 | # custom
53 | "authentication.apps.AuthenticationConfig",
54 | ]
55 |
56 | SITE_ID = 1
57 |
58 | MIDDLEWARE = [
59 | "corsheaders.middleware.CorsMiddleware",
60 | "django.middleware.security.SecurityMiddleware",
61 | "django.contrib.sessions.middleware.SessionMiddleware",
62 | "django.middleware.common.CommonMiddleware",
63 | "django.middleware.csrf.CsrfViewMiddleware",
64 | "django.contrib.auth.middleware.AuthenticationMiddleware",
65 | "django.contrib.messages.middleware.MessageMiddleware",
66 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
67 | ]
68 |
69 | ROOT_URLCONF = "core.urls"
70 |
71 | TEMPLATES = [
72 | {
73 | "BACKEND": "django.template.backends.django.DjangoTemplates",
74 | "DIRS": [BASE_DIR / "templates"],
75 | "APP_DIRS": True,
76 | "OPTIONS": {
77 | "context_processors": [
78 | "django.template.context_processors.debug",
79 | "django.template.context_processors.request",
80 | "django.contrib.auth.context_processors.auth",
81 | "django.contrib.messages.context_processors.messages",
82 | ],
83 | },
84 | },
85 | ]
86 |
87 | WSGI_APPLICATION = "core.wsgi.application"
88 |
89 |
90 | # Database
91 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
92 |
93 | DATABASES = {
94 | "default": {
95 | "ENGINE": "django.db.backends.sqlite3",
96 | "NAME": BASE_DIR / "db.sqlite3",
97 | }
98 | }
99 |
100 |
101 | # Password validation
102 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
103 |
104 | AUTH_PASSWORD_VALIDATORS = [
105 | {
106 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
107 | },
108 | {
109 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
110 | },
111 | {
112 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
113 | },
114 | {
115 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
116 | },
117 | ]
118 |
119 |
120 | # django-cors-headers
121 | # https://pypi.org/project/django-cors-headers/
122 |
123 | if DEBUG:
124 | CORS_ALLOW_ALL_ORIGINS = True
125 | else:
126 | CORS_ALLOWED_ORIGINS = [
127 | "http://localhost:3000/",
128 | "http://127.0.0.1:3000/",
129 | ]
130 |
131 |
132 | # djangorestframework
133 | # https://www.django-rest-framework.org/
134 |
135 | REST_FRAMEWORK = {
136 | "DEFAULT_AUTHENTICATION_CLASSES": [
137 | "rest_framework_simplejwt.authentication.JWTAuthentication",
138 | ]
139 | }
140 |
141 |
142 | # django-all-auth
143 | # https://django-allauth.readthedocs.io/en/latest/index.html
144 |
145 | ACCOUNT_EMAIL_REQUIRED = False
146 | ACCOUNT_EMAIL_VERIFICATION = "none"
147 |
148 | SOCIALACCOUNT_PROVIDERS = {
149 | "google": {
150 | "APP": {
151 | "client_id": "",
152 | "secret": "",
153 | "key": "", # leave empty
154 | },
155 | "SCOPE": [
156 | "profile",
157 | "email",
158 | ],
159 | "AUTH_PARAMS": {
160 | "access_type": "online",
161 | },
162 | "VERIFIED_EMAIL": True,
163 | },
164 | }
165 |
166 |
167 | # dj-rest-auth & djangorestframework-simplejwt
168 | # https://dj-rest-auth.readthedocs.io/en/latest/index.html
169 | # https://django-rest-framework-simplejwt.readthedocs.io/en/latest/index.html
170 |
171 | REST_AUTH = {
172 | "USE_JWT": True,
173 | "JWT_AUTH_HTTPONLY": False,
174 | }
175 |
176 | SIMPLE_JWT = {
177 | "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
178 | "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
179 | "ROTATE_REFRESH_TOKENS": False,
180 | "BLACKLIST_AFTER_ROTATION": False,
181 | "UPDATE_LAST_LOGIN": True,
182 | "SIGNING_KEY": "jwt-insecure-z6dh*i8cjajq$o6lg-@$%3v06vpl!irr9+v0=+d&5d$f#-(t",
183 | "ALGORITHM": "HS512",
184 | }
185 |
186 |
187 | # Internationalization
188 | # https://docs.djangoproject.com/en/4.2/topics/i18n/
189 |
190 | LANGUAGE_CODE = "en-us"
191 |
192 | TIME_ZONE = "UTC"
193 |
194 | USE_I18N = True
195 |
196 | USE_TZ = True
197 |
198 |
199 | # Static files (CSS, JavaScript, Images)
200 | # https://docs.djangoproject.com/en/4.2/howto/static-files/
201 |
202 | STATIC_URL = "static/"
203 |
204 |
205 | # Default primary key field type
206 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
207 |
208 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
209 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/pycharm+all,django
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all,django
3 |
4 | ### Django ###
5 | *.log
6 | *.pot
7 | *.pyc
8 | __pycache__/
9 | local_settings.py
10 | db.sqlite3
11 | db.sqlite3-journal
12 | media
13 |
14 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
15 | # in your Git repository. Update and uncomment the following line accordingly.
16 | # /staticfiles/
17 |
18 | ### Django.Python Stack ###
19 | # Byte-compiled / optimized / DLL files
20 | *.py[cod]
21 | *$py.class
22 |
23 | # C extensions
24 | *.so
25 |
26 | # Distribution / packaging
27 | .Python
28 | build/
29 | develop-eggs/
30 | dist/
31 | downloads/
32 | eggs/
33 | .eggs/
34 | lib/
35 | lib64/
36 | parts/
37 | sdist/
38 | var/
39 | wheels/
40 | share/python-wheels/
41 | *.egg-info/
42 | .installed.cfg
43 | *.egg
44 | MANIFEST
45 |
46 | # PyInstaller
47 | # Usually these files are written by a python script from a template
48 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
49 | *.manifest
50 | *.spec
51 |
52 | # Installer logs
53 | pip-log.txt
54 | pip-delete-this-directory.txt
55 |
56 | # Unit test / coverage reports
57 | htmlcov/
58 | .tox/
59 | .nox/
60 | .coverage
61 | .coverage.*
62 | .cache
63 | nosetests.xml
64 | coverage.xml
65 | *.cover
66 | *.py,cover
67 | .hypothesis/
68 | .pytest_cache/
69 | cover/
70 |
71 | # Translations
72 | *.mo
73 |
74 | # Django stuff:
75 |
76 | # Flask stuff:
77 | instance/
78 | .webassets-cache
79 |
80 | # Scrapy stuff:
81 | .scrapy
82 |
83 | # Sphinx documentation
84 | docs/_build/
85 |
86 | # PyBuilder
87 | .pybuilder/
88 | target/
89 |
90 | # Jupyter Notebook
91 | .ipynb_checkpoints
92 |
93 | # IPython
94 | profile_default/
95 | ipython_config.py
96 |
97 | # pyenv
98 | # For a library or package, you might want to ignore these files since the code is
99 | # intended to run in multiple environments; otherwise, check them in:
100 | # .python-version
101 |
102 | # pipenv
103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
106 | # install all needed dependencies.
107 | #Pipfile.lock
108 |
109 | # poetry
110 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
111 | # This is especially recommended for binary packages to ensure reproducibility, and is more
112 | # commonly ignored for libraries.
113 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
114 | #poetry.lock
115 |
116 | # pdm
117 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
118 | #pdm.lock
119 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
120 | # in version control.
121 | # https://pdm.fming.dev/#use-with-ide
122 | .pdm.toml
123 |
124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
125 | __pypackages__/
126 |
127 | # Celery stuff
128 | celerybeat-schedule
129 | celerybeat.pid
130 |
131 | # SageMath parsed files
132 | *.sage.py
133 |
134 | # Environments
135 | .env
136 | .venv
137 | env/
138 | venv/
139 | ENV/
140 | env.bak/
141 | venv.bak/
142 |
143 | # Spyder project settings
144 | .spyderproject
145 | .spyproject
146 |
147 | # Rope project settings
148 | .ropeproject
149 |
150 | # mkdocs documentation
151 | /site
152 |
153 | # mypy
154 | .mypy_cache/
155 | .dmypy.json
156 | dmypy.json
157 |
158 | # Pyre type checker
159 | .pyre/
160 |
161 | # pytype static type analyzer
162 | .pytype/
163 |
164 | # Cython debug symbols
165 | cython_debug/
166 |
167 | # PyCharm
168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
170 | # and can be added to the global gitignore or merged into this file. For a more nuclear
171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
172 | #.idea/
173 |
174 | ### PyCharm+all ###
175 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
176 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
177 |
178 | # User-specific stuff
179 | .idea/**/workspace.xml
180 | .idea/**/tasks.xml
181 | .idea/**/usage.statistics.xml
182 | .idea/**/dictionaries
183 | .idea/**/shelf
184 |
185 | # AWS User-specific
186 | .idea/**/aws.xml
187 |
188 | # Generated files
189 | .idea/**/contentModel.xml
190 |
191 | # Sensitive or high-churn files
192 | .idea/**/dataSources/
193 | .idea/**/dataSources.ids
194 | .idea/**/dataSources.local.xml
195 | .idea/**/sqlDataSources.xml
196 | .idea/**/dynamic.xml
197 | .idea/**/uiDesigner.xml
198 | .idea/**/dbnavigator.xml
199 |
200 | # Gradle
201 | .idea/**/gradle.xml
202 | .idea/**/libraries
203 |
204 | # Gradle and Maven with auto-import
205 | # When using Gradle or Maven with auto-import, you should exclude module files,
206 | # since they will be recreated, and may cause churn. Uncomment if using
207 | # auto-import.
208 | # .idea/artifacts
209 | # .idea/compiler.xml
210 | # .idea/jarRepositories.xml
211 | # .idea/modules.xml
212 | # .idea/*.iml
213 | # .idea/modules
214 | # *.iml
215 | # *.ipr
216 |
217 | # CMake
218 | cmake-build-*/
219 |
220 | # Mongo Explorer plugin
221 | .idea/**/mongoSettings.xml
222 |
223 | # File-based project format
224 | *.iws
225 |
226 | # IntelliJ
227 | out/
228 |
229 | # mpeltonen/sbt-idea plugin
230 | .idea_modules/
231 |
232 | # JIRA plugin
233 | atlassian-ide-plugin.xml
234 |
235 | # Cursive Clojure plugin
236 | .idea/replstate.xml
237 |
238 | # SonarLint plugin
239 | .idea/sonarlint/
240 |
241 | # Crashlytics plugin (for Android Studio and IntelliJ)
242 | com_crashlytics_export_strings.xml
243 | crashlytics.properties
244 | crashlytics-build.properties
245 | fabric.properties
246 |
247 | # Editor-based Rest Client
248 | .idea/httpRequests
249 |
250 | # Android studio 3.1+ serialized cache file
251 | .idea/caches/build_file_checksums.ser
252 |
253 | ### PyCharm+all Patch ###
254 | # Ignore everything but code style settings and run configurations
255 | # that are supposed to be shared within teams.
256 |
257 | .idea/*
258 |
259 | !.idea/codeStyles
260 | !.idea/runConfigurations
261 |
262 | # End of https://www.toptal.com/developers/gitignore/api/pycharm+all,django
--------------------------------------------------------------------------------