├── .gitignore ├── ApiRoot ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── Dockerfile ├── README.md ├── auth ├── __init__.py └── views.py ├── docker-compose.yaml ├── frontend ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── next.svg │ └── vercel.svg ├── src │ ├── app │ │ ├── auth │ │ │ ├── password │ │ │ │ ├── reset-password-confirmation │ │ │ │ │ └── page.tsx │ │ │ │ └── reset-password │ │ │ │ │ └── page.tsx │ │ │ ├── register │ │ │ │ └── page.tsx │ │ │ └── utils.ts │ │ ├── components │ │ │ ├── Login.tsx │ │ │ ├── Register.tsx │ │ │ ├── ResetPassword.tsx │ │ │ └── ResetPasswordConfirmation.tsx │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── fetcher.ts │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ └── middleware.ts ├── tailwind.config.ts └── tsconfig.json ├── manage.py ├── requirements.txt └── setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | -------------------------------------------------------------------------------- /ApiRoot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koladev32/django-nextjs-auth/38433766459f20e988822f9aefab9f83f3c0a3b2/ApiRoot/__init__.py -------------------------------------------------------------------------------- /ApiRoot/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for ApiRoot 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", "ApiRoot.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ApiRoot/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for ApiRoot project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.4. 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 | 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 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = "django-insecure-us)-)2ol=f725b_aq+#!2b)bupxd58y67ir3!=-q663vb_moyy" 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | "django.contrib.admin", 33 | "django.contrib.auth", 34 | "django.contrib.contenttypes", 35 | "django.contrib.sessions", 36 | "django.contrib.messages", 37 | "django.contrib.staticfiles", 38 | # installed apps 39 | "rest_framework", 40 | "djoser", 41 | "corsheaders", 42 | "rest_framework_simplejwt.token_blacklist", 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | "django.middleware.security.SecurityMiddleware", 47 | "django.contrib.sessions.middleware.SessionMiddleware", 48 | "corsheaders.middleware.CorsMiddleware", 49 | "django.middleware.common.CommonMiddleware", 50 | "django.middleware.csrf.CsrfViewMiddleware", 51 | "django.contrib.auth.middleware.AuthenticationMiddleware", 52 | "django.contrib.messages.middleware.MessageMiddleware", 53 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 54 | ] 55 | 56 | ROOT_URLCONF = "ApiRoot.urls" 57 | 58 | TEMPLATES = [ 59 | { 60 | "BACKEND": "django.template.backends.django.DjangoTemplates", 61 | "DIRS": [], 62 | "APP_DIRS": True, 63 | "OPTIONS": { 64 | "context_processors": [ 65 | "django.template.context_processors.debug", 66 | "django.template.context_processors.request", 67 | "django.contrib.auth.context_processors.auth", 68 | "django.contrib.messages.context_processors.messages", 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = "ApiRoot.wsgi.application" 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 78 | 79 | DATABASES = { 80 | "default": { 81 | "ENGINE": "django.db.backends.sqlite3", 82 | "NAME": BASE_DIR / "db.sqlite3", 83 | } 84 | } 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 92 | }, 93 | { 94 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 101 | }, 102 | ] 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 106 | 107 | LANGUAGE_CODE = "en-us" 108 | 109 | TIME_ZONE = "UTC" 110 | 111 | USE_I18N = True 112 | 113 | USE_TZ = True 114 | 115 | # Static files (CSS, JavaScript, Images) 116 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 117 | 118 | STATIC_URL = "static/" 119 | 120 | # Default primary key field type 121 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 122 | 123 | 124 | SITE_NAME = "Test Django Next.js" 125 | 126 | DOMAIN = 'localhost:3000' 127 | 128 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 129 | 130 | REST_FRAMEWORK = { 131 | "DEFAULT_AUTHENTICATION_CLASSES": ( 132 | "rest_framework_simplejwt.authentication.JWTAuthentication", 133 | ), 134 | } 135 | 136 | SIMPLE_JWT = { 137 | "AUTH_HEADER_TYPES": ("Bearer",), 138 | } 139 | 140 | CORS_ALLOWED_ORIGINS = [ 141 | "http://localhost:3000", 142 | "http://127.0.0.1:3000", 143 | ] 144 | 145 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 146 | 147 | DJOSER = { 148 | "PASSWORD_RESET_CONFIRM_URL": "auth/password/reset-password-confirmation/?uid={uid}&token={token}", 149 | "ACTIVATION_URL": "#/activate/{uid}/{token}", 150 | "SEND_ACTIVATION_EMAIL": False, 151 | "SERIALIZERS": {}, 152 | "LOGIN_FIELD": "email" 153 | } 154 | -------------------------------------------------------------------------------- /ApiRoot/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for ApiRoot 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 | 18 | from django.contrib import admin 19 | from django.urls import path, include 20 | 21 | from auth.views import LogoutView 22 | 23 | urlpatterns = [ 24 | path("admin/", admin.site.urls), 25 | path("auth/", include("djoser.urls")), 26 | path("auth/", include("djoser.urls.jwt")), 27 | path("auth/logout/", LogoutView.as_view()), 28 | ] 29 | -------------------------------------------------------------------------------- /ApiRoot/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for ApiRoot 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", "ApiRoot.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.11 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set the working directory in the container 9 | WORKDIR /app 10 | 11 | # Copy the current directory contents into the container at /app 12 | COPY . /app 13 | 14 | # Install any needed packages specified in requirements.txt 15 | RUN pip install --upgrade pip && pip install -r requirements.txt 16 | 17 | # Make port 8000 available to the world outside this container 18 | EXPOSE 8000 19 | 20 | # Run Django server 21 | CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django & Next.js Authentication fullstack application 2 | 3 | This is an application built with Next.js and Django to showcase how to build a protected application with authentication features such as registration, login, reset password, and session handling on the front-end. 4 | 5 | You can find an implementation of this application with Redux added. It can be useful for starting a project with Redux Toolkit and Next.js.☺️ 6 | 7 | ## Stack used 8 | - Django and DRF for the backend 9 | - Next.js and Tailwind on the frontend 10 | - Wretch for API calls 11 | - Djoser for authentication 12 | 13 | ## Setup 14 | 15 | ### With Docker 16 | 17 | ```shell 18 | docker compose up -d --build 19 | ``` 20 | 21 | By default, the Django backend will be running on localhost:8000 and the frontend on localhost:3000. 22 | 23 | ## Manual setup 24 | 25 | ```bash 26 | git clone https://github.com/koladev32/django-nextjs-auth.git && cd django-nextjs-auth 27 | ``` 28 | 29 | Run the `setup.sh` script to handle dependencies installation on the backend and the frontend. 30 | 31 | ```shell 32 | chmod +x setup.sh 33 | ./setup.sh 34 | ``` 35 | 36 | To start the application, run the following commands. 37 | 38 | ```bash 39 | source venv/bin/activate 40 | python manage.py runserver 41 | ``` 42 | 43 | The commands above will start the backend application. To start the frontend, run the following commands. 44 | 45 | ```bash 46 | cd frontend 47 | npm run dev 48 | ``` 49 | 50 | Made with ❤️ 51 | -------------------------------------------------------------------------------- /auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koladev32/django-nextjs-auth/38433766459f20e988822f9aefab9f83f3c0a3b2/auth/__init__.py -------------------------------------------------------------------------------- /auth/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework_simplejwt.tokens import RefreshToken 2 | from rest_framework_simplejwt.exceptions import TokenError 3 | from rest_framework.views import APIView 4 | from rest_framework.permissions import AllowAny 5 | from rest_framework import status 6 | from rest_framework.response import Response 7 | 8 | from django.core.exceptions import ObjectDoesNotExist 9 | 10 | 11 | class LogoutView(APIView): 12 | permission_classes = (AllowAny,) 13 | authentication_classes = () 14 | 15 | def post(self, request): 16 | try: 17 | refresh_token = request.data["refresh"] 18 | token = RefreshToken(refresh_token) 19 | token.blacklist() 20 | return Response(status=status.HTTP_200_OK) 21 | except (ObjectDoesNotExist, TokenError): 22 | return Response(status=status.HTTP_400_BAD_REQUEST) 23 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | backend: 5 | build: . 6 | volumes: 7 | - .:/app 8 | ports: 9 | - "8000:8000" 10 | environment: 11 | - PYTHONDONTWRITEBYTECODE=1 12 | - PYTHONUNBUFFERED=1 13 | 14 | frontend: 15 | build: 16 | context: ./frontend 17 | dockerfile: Dockerfile 18 | volumes: 19 | - ./frontend:/app 20 | ports: 21 | - "3000:3000" 22 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node runtime as a parent image 2 | FROM node:latest 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the current directory contents into the container at /app 8 | COPY . /app 9 | 10 | # Install any needed packages 11 | RUN npm install 12 | 13 | # Make port 3000 available to the world outside this container 14 | EXPOSE 3000 15 | 16 | # Run the app 17 | CMD ["npm", "run", "dev"] 18 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /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 | "js-cookie": "^3.0.5", 13 | "next": "14.1.0", 14 | "react": "^18", 15 | "react-dom": "^18", 16 | "react-hook-form": "^7.50.1", 17 | "swr": "^2.2.4", 18 | "wretch": "^2.8.0" 19 | }, 20 | "devDependencies": { 21 | "@types/js-cookie": "^3.0.6", 22 | "@types/node": "^20", 23 | "@types/react": "^18", 24 | "@types/react-dom": "^18", 25 | "autoprefixer": "^10.0.1", 26 | "eslint": "^8", 27 | "eslint-config-next": "14.1.0", 28 | "postcss": "^8", 29 | "prettier": "^3.2.5", 30 | "tailwindcss": "^3.3.0", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/auth/password/reset-password-confirmation/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ResetPasswordConfirmation from "@/app/components/ResetPasswordConfirmation"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/auth/password/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ResetPassword from "@/app/components/ResetPassword"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Register from "@/app/components/Register"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/auth/utils.ts: -------------------------------------------------------------------------------- 1 | import wretch from "wretch"; 2 | import Cookies from "js-cookie"; 3 | 4 | // Base API setup for making HTTP requests 5 | const api = wretch("http://localhost:8000").accept("application/json"); 6 | 7 | /** 8 | * Stores a token in cookies. 9 | * @param {string} token - The token to be stored. 10 | * @param {"access" | "refresh"} type - The type of the token (access or refresh). 11 | */ 12 | const storeToken = (token: string, type: "access" | "refresh") => { 13 | Cookies.set(type + "Token", token); 14 | }; 15 | 16 | /** 17 | * Retrieves a token from cookies. 18 | * @param {"access" | "refresh"} type - The type of the token to retrieve (access or refresh). 19 | * @returns {string | undefined} The token, if found. 20 | */ 21 | const getToken = (type: string) => { 22 | return Cookies.get(type + "Token"); 23 | }; 24 | 25 | /** 26 | * Removes both access and refresh tokens from cookies. 27 | */ 28 | const removeTokens = () => { 29 | Cookies.remove("accessToken"); 30 | Cookies.remove("refreshToken"); 31 | }; 32 | 33 | /** 34 | * Registers a new user. 35 | * @param {string} email - The email of the account. 36 | * @param {string} username - The username of the account. 37 | * @param {string} password - The password for the account. 38 | * @returns {Promise} A promise that resolves with the registration response. 39 | */ 40 | const register = (email: string, username: string, password: string) => { 41 | return api.post({ email, username, password }, "/auth/users/"); 42 | }; 43 | 44 | /** 45 | * Logs in a user and stores access and refresh tokens. 46 | * @param {string} email - The user's email. 47 | * @param {string} password - The user's password. 48 | * @returns {Promise} A promise that resolves with the login response. 49 | */ 50 | const login = (email: string, password: string) => { 51 | return api.post({ username: email, password }, "/auth/jwt/create"); 52 | }; 53 | 54 | /** 55 | * Logout a user. 56 | * @returns {Promise} A promise that resolves with the login response. 57 | */ 58 | const logout = () => { 59 | const refreshToken = getToken("refresh"); 60 | return api.post({ refresh: refreshToken }, "/auth/logout/"); 61 | }; 62 | 63 | /** 64 | * Refreshes the JWT token using the stored refresh token. 65 | * @returns {Promise} A promise that resolves with the new access token. 66 | */ 67 | const handleJWTRefresh = () => { 68 | const refreshToken = getToken("refresh"); 69 | return api.post({ refresh: refreshToken }, "/auth/jwt/refresh"); 70 | }; 71 | 72 | /** 73 | * Initiates a password reset request. 74 | * @param {string} email - The email of the user requesting a password reset. 75 | * @returns {Promise} A promise that resolves with the password reset response. 76 | */ 77 | const resetPassword = (email: string) => { 78 | return api.post({ email }, "/auth/users/reset_password/"); 79 | }; 80 | 81 | /** 82 | * Confirms the password reset with new password details. 83 | * @param {string} new_password - The new password. 84 | * @param {string} re_new_password - Confirmation of the new password. 85 | * @param {string} token - The token for authenticating the password reset request. 86 | * @param {string} uid - The user ID. 87 | * @returns {Promise} A promise that resolves with the password reset confirmation response. 88 | */ 89 | const resetPasswordConfirm = ( 90 | new_password: string, 91 | re_new_password: string, 92 | token: string, 93 | uid: string 94 | ) => { 95 | return api.post( 96 | { uid, token, new_password, re_new_password }, 97 | "/auth/users/reset_password_confirm/" 98 | ); 99 | }; 100 | 101 | /** 102 | * Exports authentication-related actions. 103 | * @returns {Object} An object containing all the auth actions. 104 | */ 105 | export const AuthActions = () => { 106 | return { 107 | login, 108 | resetPasswordConfirm, 109 | handleJWTRefresh, 110 | register, 111 | resetPassword, 112 | storeToken, 113 | getToken, 114 | logout, 115 | removeTokens, 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /frontend/src/app/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useForm } from "react-hook-form"; 3 | import { AuthActions } from "@/app/auth/utils"; 4 | import { useRouter } from "next/navigation"; 5 | import Link from "next/link"; 6 | 7 | type FormData = { 8 | email: string; 9 | password: string; 10 | }; 11 | 12 | const Login = () => { 13 | const { 14 | register, 15 | handleSubmit, 16 | formState: { errors }, 17 | setError, 18 | } = useForm(); 19 | 20 | const router = useRouter(); 21 | 22 | const { login, storeToken } = AuthActions(); 23 | 24 | const onSubmit = (data: FormData) => { 25 | login(data.email, data.password) 26 | .json((json) => { 27 | storeToken(json.access, "access"); 28 | storeToken(json.refresh, "refresh"); 29 | 30 | router.push("dashboard"); 31 | }) 32 | .catch((err) => { 33 | setError("root", { type: "manual", message: err.json.detail }); 34 | }); 35 | }; 36 | 37 | return ( 38 |
39 |
40 |

Login to your account

41 |
42 |
43 | 46 | 52 | {errors.email && ( 53 | Email is required 54 | )} 55 |
56 |
57 | 60 | 66 | {errors.password && ( 67 | Password is required 68 | )} 69 |
70 |
71 | 74 |
75 | {errors.root && ( 76 | {errors.root.message} 77 | )} 78 |
79 |
80 | 84 | Forgot password? 85 | 86 |
87 |
88 |
89 | ); 90 | }; 91 | 92 | export default Login; 93 | -------------------------------------------------------------------------------- /frontend/src/app/components/Register.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useForm } from "react-hook-form"; 3 | import { AuthActions } from "@/app/auth/utils"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | type FormData = { 7 | email: string; 8 | username: string; 9 | password: string; 10 | }; 11 | 12 | const Register = () => { 13 | const { 14 | register, 15 | handleSubmit, 16 | formState: { errors }, 17 | setError, 18 | } = useForm(); 19 | 20 | const router = useRouter(); 21 | 22 | const { register: registerUser } = AuthActions(); // Note: Renamed to avoid naming conflict with useForm's register 23 | 24 | const onSubmit = (data: FormData) => { 25 | registerUser(data.email, data.username, data.password) 26 | .json(() => { 27 | router.push("/"); // Adjust the path as needed 28 | }) 29 | .catch((err) => { 30 | setError("root", { 31 | type: "manual", 32 | message: err.json.detail, 33 | }); 34 | }); 35 | }; 36 | 37 | return ( 38 |
39 |
40 |

Register your account

41 |
42 |
43 | 46 | 52 | {errors.email && ( 53 | 54 | {errors.email.message} 55 | 56 | )} 57 |
58 |
59 | 62 | 68 | {errors.username && ( 69 | 70 | {errors.username.message} 71 | 72 | )} 73 |
74 |
75 | 78 | 84 | {errors.password && ( 85 | 86 | {errors.password.message} 87 | 88 | )} 89 |
90 |
91 | 94 |
95 | {errors.root && ( 96 | {errors.root.message} 97 | )} 98 |
99 |
100 |
101 | ); 102 | }; 103 | 104 | export default Register; 105 | -------------------------------------------------------------------------------- /frontend/src/app/components/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useForm } from "react-hook-form"; 3 | import { AuthActions } from "@/app/auth/utils"; 4 | 5 | type FormData = { 6 | email: string; 7 | }; 8 | 9 | const ResetPassword = () => { 10 | const { 11 | register, 12 | handleSubmit, 13 | formState: { errors }, 14 | } = useForm(); 15 | const { resetPassword } = AuthActions(); 16 | 17 | const onSubmit = async (data: FormData) => { 18 | try { 19 | await resetPassword(data.email).res(); 20 | alert("Password reset email sent. Please check your inbox."); 21 | } catch (err) { 22 | alert("Failed to send password reset email. Please try again."); 23 | } 24 | }; 25 | 26 | return ( 27 |
28 |
29 |

Reset Password

30 |
31 | 34 | 40 | {errors.email && ( 41 | Email is required 42 | )} 43 |
44 | 47 |
48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default ResetPassword; 55 | -------------------------------------------------------------------------------- /frontend/src/app/components/ResetPasswordConfirmation.tsx: -------------------------------------------------------------------------------- 1 | // pages/reset-password-confirmation.js 2 | import React, { useEffect, useState } from "react"; 3 | import { useForm } from "react-hook-form"; 4 | import { AuthActions } from "@/app/auth/utils"; 5 | import { useSearchParams, useRouter } from "next/navigation"; 6 | type FormData = { 7 | password: string; 8 | }; 9 | 10 | const ResetPasswordConfirmation = () => { 11 | const { 12 | register, 13 | handleSubmit, 14 | formState: { errors }, 15 | } = useForm(); 16 | const router = useRouter(); 17 | const { resetPasswordConfirm } = AuthActions(); 18 | 19 | const searchParams = useSearchParams(); 20 | 21 | // State for UID and Token 22 | const [uid, setUid] = useState(""); 23 | const [token, setToken] = useState(""); 24 | 25 | // Extract UID and Token from URL 26 | useEffect(() => { 27 | if (searchParams.get("uid") && searchParams.get("token")) { 28 | setUid(searchParams.get("uid") as string); 29 | setToken(searchParams.get("token") as string); 30 | } 31 | }, [searchParams]); 32 | 33 | const onSubmit = async (data: FormData) => { 34 | try { 35 | await resetPasswordConfirm( 36 | data.password, 37 | data.password, 38 | token, 39 | uid, 40 | ).res(); 41 | alert("Password has been reset successfully."); 42 | router.push("/"); 43 | } catch (err) { 44 | alert("Failed to reset password. Please try again."); 45 | } 46 | }; 47 | 48 | return ( 49 |
50 |
51 |

Set New Password

52 |
53 | 56 | 62 | {errors.password && ( 63 | Password is required 64 | )} 65 |
66 | 69 |
70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default ResetPasswordConfirmation; 77 | -------------------------------------------------------------------------------- /frontend/src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useSWR from "swr"; 4 | import { fetcher } from "@/app/fetcher"; 5 | import { AuthActions } from "@/app/auth/utils"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | export default function Home() { 9 | const router = useRouter(); 10 | 11 | const { data: user } = useSWR("/auth/users/me", fetcher); 12 | 13 | const { logout, removeTokens } = AuthActions(); 14 | 15 | const handleLogout = () => { 16 | logout() 17 | .res(() => { 18 | removeTokens(); 19 | 20 | router.push("/"); 21 | }) 22 | .catch(() => { 23 | removeTokens(); 24 | router.push("/"); 25 | }); 26 | }; 27 | 28 | return ( 29 |
30 |
31 |

Hi, {user?.username}!

32 |

Your account details:

33 |
    34 |
  • Username: {user?.username}
  • 35 |
  • Email: {user?.email}
  • 36 |
37 | 43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koladev32/django-nextjs-auth/38433766459f20e988822f9aefab9f83f3c0a3b2/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/fetcher.ts: -------------------------------------------------------------------------------- 1 | import wretch, { Wretch, WretchError } from "wretch"; 2 | import { AuthActions } from "@/app/auth/utils"; 3 | 4 | // Extract necessary functions from the AuthActions utility. 5 | const { handleJWTRefresh, storeToken, getToken } = AuthActions(); 6 | 7 | /** 8 | * Configures the API with authentication and automatic token refresh on 401 responses. 9 | */ 10 | const api = () => { 11 | return ( 12 | wretch("http://localhost:8000") 13 | // Initialize authentication with the access token. 14 | .auth(`Bearer ${getToken("access")}`) 15 | // Catch 401 errors to refresh the token and retry the request. 16 | .catcher(401, async (error: WretchError, request: Wretch) => { 17 | try { 18 | // Attempt to refresh the JWT token. 19 | const { access } = (await handleJWTRefresh().json()) as { 20 | access: string; 21 | }; 22 | 23 | // Store the new access token. 24 | storeToken(access, "access"); 25 | 26 | // Replay the original request with the new access token. 27 | return request 28 | .auth(`Bearer ${access}`) 29 | .fetch() 30 | .unauthorized(() => { 31 | // Rethrow the error if unauthorized after token refresh. 32 | window.location.replace("/"); 33 | }) 34 | .json(); 35 | } catch (err) { 36 | window.location.replace("/"); 37 | } 38 | }) 39 | ); 40 | }; 41 | 42 | /** 43 | * Fetches data from the specified URL, automatically handling authentication and token refresh. 44 | * @returns {Promise} The promise resolving to the fetched data. 45 | * @param url 46 | */ 47 | export const fetcher = (url: string): Promise => { 48 | return api().get(url).json(); 49 | }; 50 | -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Welcome to Django Nextjs Auth", 9 | description: "A demonstration of a codebase with Django and Next.js", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Login from "@/app/components/Login"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | import { cookies } from "next/headers"; 4 | export function middleware(request: NextRequest) { 5 | const cookieStore = cookies(); 6 | const accessToken = cookieStore.get("accessToken"); 7 | 8 | if (!accessToken && request.nextUrl.pathname !== "/") { 9 | return NextResponse.redirect(new URL("/", request.url)); 10 | } 11 | } 12 | 13 | export const config = { 14 | matcher: ["/((?!api|auth|_next/static|_next/image|.*\\.png$).*)"], 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /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", "ApiRoot.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 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==5.0.1 2 | django-cors-headers==4.3.1 3 | djangorestframework==3.14.0 4 | djangorestframework-simplejwt==5.3.1 5 | djoser==2.2.2 6 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit script if any command fails 4 | set -e 5 | 6 | # Function to echo in green color for success messages 7 | echo_success() { 8 | echo -e "\033[0;32m$1\033[0m" 9 | } 10 | 11 | # Function to echo in yellow color for progress messages 12 | echo_progress() { 13 | echo -e "\033[0;33m$1\033[0m" 14 | } 15 | 16 | # Setup Backend 17 | echo_progress "Setting up backend..." 18 | python3.11 -m venv venv || { echo "Failed to create virtual environment"; exit 1; } 19 | source venv/bin/activate || { echo "Failed to activate virtual environment"; exit 1; } 20 | pip install --upgrade pip || { echo "Failed to upgrade pip"; exit 1; } 21 | pip install -r requirements.txt || { echo "Failed to install backend requirements"; exit 1; } 22 | 23 | # Django Migrations 24 | python manage.py migrate || { echo "Django migrate command failed"; exit 1; } 25 | echo_success "Done setting up backend." 26 | 27 | # Setup Frontend 28 | echo_progress "Setting up frontend..." 29 | cd frontend || { echo "Failed to change directory to frontend"; exit 1; } 30 | npm install || { echo "npm install failed"; exit 1; } 31 | echo_success "Done setting up frontend." 32 | 33 | cd .. # Go back to the root directory 34 | 35 | echo_success "Project is ready" 36 | 37 | --------------------------------------------------------------------------------