├── 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 | 9 | 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 --------------------------------------------------------------------------------