├── .gitignore ├── LICENSE ├── README.md ├── backend ├── api │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── backend │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── requirements.txt └── userauths │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ ├── 0002_remove_customuser_full_name.py │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py └── frontend ├── .eslintrc.json ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── login │ │ └── page.js │ ├── page.tsx │ └── register │ │ └── page.js ├── components │ └── Header.js ├── context │ └── AuthContext.js └── middleware.js ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Django specific 7 | *.log 8 | *.pot 9 | *.pyc 10 | *.pyo 11 | *.pyd 12 | *.sqlite3 13 | *.db 14 | local_settings.py 15 | 16 | # Local development environment 17 | **/.env 18 | .env 19 | .venv/ 20 | env/ 21 | venv/ 22 | .env.local 23 | .env*.local 24 | **/.env.local 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .coverage.* 29 | .cache 30 | *.cover 31 | *.py,cover 32 | /coverage 33 | 34 | # Distribution / packaging 35 | .Python 36 | build/ 37 | develop-eggs/ 38 | dist/ 39 | downloads/ 40 | lib/ 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | wheels/ 46 | share/python-wheels/ 47 | *.egg-info/ 48 | .installed.cfg 49 | *.egg 50 | 51 | # Front-end stuff 52 | frontend/.next/ 53 | node_modules/ 54 | npm-debug.log 55 | .yarn/install-state.gz 56 | /.pnp 57 | .pnp.js 58 | /.next/ 59 | /out/ 60 | .build 61 | /build 62 | vercel/ 63 | 64 | # Ignore local documentation files 65 | local_documentation.txt 66 | 67 | # Django static and media files 68 | /media/ 69 | /staticfiles/ 70 | 71 | # Editor specific / OS generated files 72 | .vscode/ 73 | .DS_Store 74 | *.pem 75 | 76 | # Debug 77 | npm-debug.log* 78 | yarn-debug.log* 79 | yarn-error.log* 80 | 81 | # TypeScript 82 | *.tsbuildinfo 83 | next-env.d.ts 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Kalvin Isaiah Dela Rama Calimag 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # django-nextjs-jwt-starter 3 | 4 | 5 | 6 | A sample setup integrating Django REST Framework, React/NextJS, and JWT Authentication for your development needs. 7 | 8 | 9 | 10 | ## Demo 11 | 12 | #### Main 13 | 14 | ![demo-1](https://res.cloudinary.com/dotera808/image/upload/v1724405550/Demo-1_tbvd6a.gif) 15 | 16 | #### Admin 17 | ![demo-2](https://res.cloudinary.com/dotera808/image/upload/v1724405550/Demo-2_keiyxj.gif) 18 | 19 | 20 | ## Setup 21 | 22 | 23 | 24 | Clone the repo 25 | 26 | 27 | 28 | ``` 29 | 30 | $ git clone https://github.com/kalvincalimag/django-nextjs-jwt-starter.git 31 | 32 | ``` 33 | 34 | 35 | 36 | ### Setup Backend (Django) 37 | 38 | 39 | 40 | 41 | 42 | First, create a `.env` file in the root of the backend folder & add your Django secret key: 43 | 44 | ``` 45 | 46 | DJ_SECRET_KEY= 47 | 48 | ``` 49 | 50 | For generating a key, refer [here](https://www.makeuseof.com/django-secret-key-generate-new/). 51 | 52 | 53 | Enter Directory 54 | 55 | ``` 56 | 57 | cd backend 58 | 59 | ``` 60 | 61 | 62 | 63 | Install dependencies: 64 | 65 | ``` 66 | 67 | pip install -r requirements.txt 68 | 69 | ``` 70 | 71 | 72 | 73 | Set up the database: 74 | 75 | ``` 76 | 77 | python manage.py makemigrations 78 | 79 | python manage.py migrate 80 | 81 | ``` 82 | 83 | 84 | 85 | Run server: 86 | 87 | ``` 88 | 89 | python manage.py runserver 90 | 91 | ``` 92 | 93 | 94 | 95 | ### Setup Frontend (NextJS) 96 | 97 | 98 | 99 | Enter directory 100 | 101 | 102 | 103 | ``` 104 | 105 | $ cd frontend 106 | 107 | ``` 108 | 109 | 110 | 111 | Install dependencies 112 | 113 | 114 | 115 | ``` 116 | 117 | npm install 118 | 119 | ``` 120 | 121 | 122 | 123 | Run the app 124 | 125 | 126 | 127 | ``` 128 | 129 | npm run dev 130 | 131 | ``` 132 | 133 | 134 | 135 | ## Contact 136 | 137 | 138 | 139 | ### Let's connect 140 | 141 | 142 | 143 | - Twitter [@kalvincalimag_](https://twitter.com/kalvincalimag_) 144 | 145 | 146 | 147 | ### If you find this project helpful, please consider giving it a ⭐. 148 | 149 | 150 | 151 | [⭐](https://github.com/kalvincalimag/django-nextjs-jwt-starter) this repo or follow me on: 152 | 153 | 154 | 155 | - Github [@kalvincalimag](https://github.com/kalvincalimag) 156 | 157 | - Medium [@kalvincalimag](https://medium.com/@kalvincalimag) 158 | 159 | 160 | 161 | ## License 162 | 163 | 164 | 165 | [BSD](LICENSE.md) @kalvincalimag -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalvincalimag/django-nextjs-jwt-starter/15f01b62eac89c2979a1c978fee764784e636d5d/backend/api/__init__.py -------------------------------------------------------------------------------- /backend/api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "api" 7 | -------------------------------------------------------------------------------- /backend/api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /backend/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from userauths.models import CustomUser, Profile 3 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 4 | from django.contrib.auth.password_validation import validate_password 5 | 6 | class MyTokenObtainPairSerializer(TokenObtainPairSerializer): 7 | @classmethod 8 | def get_token(cls, user): 9 | token = super().get_token(user) 10 | token['email'] = user.email 11 | token['username'] = user.username 12 | return token 13 | 14 | class RegisterSerializer(serializers.ModelSerializer): 15 | password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) 16 | password2 = serializers.CharField(write_only=True, required=True) 17 | 18 | class Meta: 19 | model = CustomUser 20 | fields = ['username', 'email', 'password', 'password2'] 21 | 22 | def validate(self, attrs): 23 | if attrs['password'] != attrs['password2']: 24 | raise serializers.ValidationError({"password": "Password fields didn't match."}) 25 | return attrs 26 | 27 | def create(self, validated_data): 28 | user = CustomUser.objects.create( 29 | username=validated_data['username'], 30 | email=validated_data['email'], 31 | ) 32 | user.set_password(validated_data['password']) 33 | user.save() 34 | return user 35 | 36 | class CustomUserSerializer(serializers.ModelSerializer): 37 | class Meta: 38 | model = CustomUser 39 | fields = ['id', 'email', 'username'] 40 | 41 | class ProfileSerializer(serializers.ModelSerializer): 42 | user = CustomUserSerializer(read_only=True) 43 | 44 | class Meta: 45 | model = Profile 46 | fields = ['id', 'user', 'avatar', 'bio', 'country'] 47 | 48 | def update(self, instance, validated_data): 49 | user_data = validated_data.pop('user', None) 50 | if user_data: 51 | user_serializer = CustomUserSerializer(instance.user, data=user_data, partial=True) 52 | if user_serializer.is_valid(): 53 | user_serializer.save() 54 | return super().update(instance, validated_data) -------------------------------------------------------------------------------- /backend/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | from rest_framework_simplejwt.views import TokenRefreshView 4 | 5 | urlpatterns = [ 6 | path("", views.get_routes), 7 | path('token/', views.MyTokenObtainPairView.as_view(), name='token_obtain_pair'), 8 | path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 9 | path('register/', views.RegisterView.as_view(), name='register'), 10 | path('profile/', views.ProfileView.as_view(), name='profile'), 11 | ] -------------------------------------------------------------------------------- /backend/api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics, permissions 2 | from rest_framework.response import Response 3 | from rest_framework.decorators import api_view 4 | from rest_framework_simplejwt.views import TokenObtainPairView 5 | from .serializers import MyTokenObtainPairSerializer, RegisterSerializer, ProfileSerializer 6 | from userauths.models import Profile 7 | 8 | @api_view(['GET']) 9 | def get_routes(request): 10 | routes = [ 11 | '/api/token', 12 | '/api/token/refresh', 13 | '/api/register', 14 | '/api/profile', 15 | ] 16 | return Response(routes) 17 | 18 | class MyTokenObtainPairView(TokenObtainPairView): 19 | serializer_class = MyTokenObtainPairSerializer 20 | 21 | class RegisterView(generics.CreateAPIView): 22 | serializer_class = RegisterSerializer 23 | permission_classes = [permissions.AllowAny] 24 | 25 | class ProfileView(generics.RetrieveUpdateAPIView): 26 | serializer_class = ProfileSerializer 27 | permission_classes = [permissions.IsAuthenticated] 28 | 29 | def get_object(self): 30 | return self.request.user.profile -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalvincalimag/django-nextjs-jwt-starter/15f01b62eac89c2979a1c978fee764784e636d5d/backend/backend/__init__.py -------------------------------------------------------------------------------- /backend/backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend 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", "backend.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.5. 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 | from dotenv import load_dotenv 15 | from datetime import timedelta 16 | import os 17 | 18 | load_dotenv() 19 | 20 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 21 | BASE_DIR = Path(__file__).resolve().parent.parent 22 | 23 | # Quick-start development settings - unsuitable for production 24 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 25 | 26 | # SECURITY WARNING: keep the secret key used in production secret! 27 | SECRET_KEY = os.getenv("DJ_SECRET_KEY") 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = True 31 | 32 | ALLOWED_HOSTS = [] 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = [ 37 | 'jazzmin', 38 | "django.contrib.admin", 39 | "django.contrib.auth", 40 | "django.contrib.contenttypes", 41 | "django.contrib.sessions", 42 | "django.contrib.messages", 43 | "django.contrib.staticfiles", 44 | 45 | # Custom Apps 46 | "api", 47 | "userauths", 48 | 49 | # Third Party Apps 50 | "rest_framework", 51 | "rest_framework_simplejwt", 52 | "rest_framework_simplejwt.token_blacklist", 53 | "corsheaders", 54 | ] 55 | 56 | MIDDLEWARE = [ 57 | "django.middleware.security.SecurityMiddleware", 58 | "django.contrib.sessions.middleware.SessionMiddleware", 59 | "corsheaders.middleware.CorsMiddleware", 60 | "django.middleware.common.CommonMiddleware", 61 | "django.middleware.csrf.CsrfViewMiddleware", 62 | "django.contrib.auth.middleware.AuthenticationMiddleware", 63 | "django.contrib.messages.middleware.MessageMiddleware", 64 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 65 | ] 66 | 67 | ROOT_URLCONF = "backend.urls" 68 | 69 | TEMPLATES = [ 70 | { 71 | "BACKEND": "django.template.backends.django.DjangoTemplates", 72 | "DIRS": [], 73 | "APP_DIRS": True, 74 | "OPTIONS": { 75 | "context_processors": [ 76 | "django.template.context_processors.debug", 77 | "django.template.context_processors.request", 78 | "django.contrib.auth.context_processors.auth", 79 | "django.contrib.messages.context_processors.messages", 80 | ], 81 | }, 82 | }, 83 | ] 84 | 85 | WSGI_APPLICATION = "backend.wsgi.application" 86 | 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 90 | 91 | DATABASES = { 92 | "default": { 93 | "ENGINE": "django.db.backends.sqlite3", 94 | "NAME": BASE_DIR / "db.sqlite3", 95 | } 96 | } 97 | 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 105 | }, 106 | { 107 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 108 | }, 109 | { 110 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 111 | }, 112 | { 113 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 114 | }, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 120 | 121 | LANGUAGE_CODE = "en-us" 122 | 123 | TIME_ZONE = "UTC" 124 | 125 | USE_I18N = True 126 | 127 | USE_TZ = True 128 | 129 | 130 | # Static files (CSS, JavaScript, Images) 131 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 132 | 133 | STATIC_URL = "static/" 134 | 135 | # Default primary key field type 136 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 137 | 138 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 139 | 140 | AUTH_USER_MODEL = 'userauths.CustomUser' 141 | 142 | REST_FRAMEWORK = { 143 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 144 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 145 | ) 146 | } 147 | 148 | SIMPLE_JWT = { 149 | "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5), 150 | "REFRESH_TOKEN_LIFETIME": timedelta(days=1), 151 | "ROTATE_REFRESH_TOKENS": True, 152 | "BLACKLIST_AFTER_ROTATION": True, 153 | "UPDATE_LAST_LOGIN": False, 154 | 155 | "ALGORITHM": "HS256", 156 | "SIGNING_KEY": SECRET_KEY, 157 | "VERIFYING_KEY": "", 158 | "AUDIENCE": None, 159 | "ISSUER": None, 160 | "JSON_ENCODER": None, 161 | "JWK_URL": None, 162 | "LEEWAY": 0, 163 | 164 | "AUTH_HEADER_TYPES": ("Bearer",), 165 | "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", 166 | "USER_ID_FIELD": "id", 167 | "USER_ID_CLAIM": "user_id", 168 | "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", 169 | 170 | "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), 171 | "TOKEN_TYPE_CLAIM": "token_type", 172 | "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", 173 | 174 | "JTI_CLAIM": "jti", 175 | 176 | "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", 177 | "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), 178 | "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), 179 | } 180 | 181 | JAZZMIN_SETTINGS = { 182 | "site_title": "App Admin", 183 | "site_header": "AppName", 184 | "site_brand": "AppName", 185 | # "site_logo": "books/img/logo.png", 186 | "login_logo": None, 187 | "login_logo_dark": None, 188 | "site_logo_classes": "img-circle", 189 | "site_icon": None, 190 | "welcome_sign": "Welcome to the AppName Admin", 191 | "copyright": "AppName Admin Ltd", 192 | "search_model": ["auth.User", "auth.Group"], 193 | "user_avatar": None, 194 | 195 | # Top Menu # 196 | "topmenu_links": [ 197 | {"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]}, 198 | {"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True}, 199 | {"model": "auth.User"}, 200 | {"app": "books"}, 201 | ], 202 | 203 | # User Menu # 204 | "usermenu_links": [ 205 | {"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True}, 206 | {"model": "auth.user"} 207 | ], 208 | 209 | # Side Menu # 210 | "show_sidebar": True, 211 | 212 | "navigation_expanded": True, 213 | "hide_apps": [], 214 | "hide_models": [], 215 | "order_with_respect_to": ["auth", "books", "books.author", "books.book"], 216 | "custom_links": { 217 | "books": [{ 218 | "name": "Make Messages", 219 | "url": "make_messages", 220 | "icon": "fas fa-comments", 221 | "permissions": ["books.view_book"] 222 | }] 223 | }, 224 | 225 | "icons": { 226 | "auth": "fas fa-users-cog", 227 | "auth.user": "fas fa-user", 228 | "auth.Group": "fas fa-users", 229 | }, 230 | "default_icon_parents": "fas fa-chevron-circle-right", 231 | "default_icon_children": "fas fa-circle", 232 | "related_modal_active": False, 233 | "custom_css": None, 234 | "custom_js": None, 235 | "use_google_fonts_cdn": True, 236 | "show_ui_builder": True, 237 | "changeform_format": "horizontal_tabs", 238 | "changeform_format_overrides": {"auth.user": "collapsible", "auth.group": "vertical_tabs"}, 239 | } 240 | 241 | JAZZMIN_UI_TWEAKS = { 242 | "navbar_small_text": False, 243 | "footer_small_text": False, 244 | "body_small_text": False, 245 | "brand_small_text": False, 246 | "brand_colour": "navbar-dark", 247 | "accent": "accent-primary", 248 | "navbar": "navbar-white navbar-light", 249 | "no_navbar_border": False, 250 | "navbar_fixed": True, 251 | "layout_boxed": False, 252 | "footer_fixed": False, 253 | "sidebar_fixed": False, 254 | "sidebar": "sidebar-dark-primary", 255 | "sidebar_nav_small_text": False, 256 | "sidebar_disable_expand": False, 257 | "sidebar_nav_child_indent": False, 258 | "sidebar_nav_compact_style": False, 259 | "sidebar_nav_legacy_style": False, 260 | "sidebar_nav_flat_style": False, 261 | "theme": "lux", 262 | "dark_mode_theme": None, 263 | "button_classes": { 264 | "primary": "btn-outline-primary", 265 | "secondary": "btn-outline-secondary", 266 | "info": "btn-info", 267 | "warning": "btn-warning", 268 | "danger": "btn-danger", 269 | "success": "btn-success" 270 | }, 271 | "actions_sticky_top": False 272 | } 273 | 274 | CORS_ALLOW_ALL_ORIGINS = True 275 | -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path("admin/", admin.site.urls), 6 | path("api/", include('api.urls')), 7 | ] 8 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend 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", "backend.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /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", "backend.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 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalvincalimag/django-nextjs-jwt-starter/15f01b62eac89c2979a1c978fee764784e636d5d/backend/requirements.txt -------------------------------------------------------------------------------- /backend/userauths/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalvincalimag/django-nextjs-jwt-starter/15f01b62eac89c2979a1c978fee764784e636d5d/backend/userauths/__init__.py -------------------------------------------------------------------------------- /backend/userauths/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import CustomUser, Profile 3 | 4 | admin.site.register(CustomUser) 5 | admin.site.register(Profile) -------------------------------------------------------------------------------- /backend/userauths/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserauthsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "userauths" 7 | -------------------------------------------------------------------------------- /backend/userauths/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2024-08-23 02:37 2 | 3 | from django.conf import settings 4 | import django.contrib.auth.models 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import django.utils.timezone 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ("auth", "0012_alter_user_first_name_max_length"), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name="CustomUser", 21 | fields=[ 22 | ( 23 | "id", 24 | models.BigAutoField( 25 | auto_created=True, 26 | primary_key=True, 27 | serialize=False, 28 | verbose_name="ID", 29 | ), 30 | ), 31 | ("password", models.CharField(max_length=128, verbose_name="password")), 32 | ( 33 | "last_login", 34 | models.DateTimeField( 35 | blank=True, null=True, verbose_name="last login" 36 | ), 37 | ), 38 | ( 39 | "is_superuser", 40 | models.BooleanField( 41 | default=False, 42 | help_text="Designates that this user has all permissions without explicitly assigning them.", 43 | verbose_name="superuser status", 44 | ), 45 | ), 46 | ( 47 | "first_name", 48 | models.CharField( 49 | blank=True, max_length=150, verbose_name="first name" 50 | ), 51 | ), 52 | ( 53 | "last_name", 54 | models.CharField( 55 | blank=True, max_length=150, verbose_name="last name" 56 | ), 57 | ), 58 | ( 59 | "is_staff", 60 | models.BooleanField( 61 | default=False, 62 | help_text="Designates whether the user can log into this admin site.", 63 | verbose_name="staff status", 64 | ), 65 | ), 66 | ( 67 | "is_active", 68 | models.BooleanField( 69 | default=True, 70 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 71 | verbose_name="active", 72 | ), 73 | ), 74 | ( 75 | "date_joined", 76 | models.DateTimeField( 77 | default=django.utils.timezone.now, verbose_name="date joined" 78 | ), 79 | ), 80 | ("email", models.EmailField(max_length=254, unique=True)), 81 | ("username", models.CharField(max_length=100, unique=True)), 82 | ("full_name", models.CharField(blank=True, max_length=100)), 83 | ( 84 | "groups", 85 | models.ManyToManyField( 86 | blank=True, 87 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 88 | related_name="user_set", 89 | related_query_name="user", 90 | to="auth.group", 91 | verbose_name="groups", 92 | ), 93 | ), 94 | ( 95 | "user_permissions", 96 | models.ManyToManyField( 97 | blank=True, 98 | help_text="Specific permissions for this user.", 99 | related_name="user_set", 100 | related_query_name="user", 101 | to="auth.permission", 102 | verbose_name="user permissions", 103 | ), 104 | ), 105 | ], 106 | options={ 107 | "verbose_name": "user", 108 | "verbose_name_plural": "users", 109 | "abstract": False, 110 | }, 111 | managers=[ 112 | ("objects", django.contrib.auth.models.UserManager()), 113 | ], 114 | ), 115 | migrations.CreateModel( 116 | name="Profile", 117 | fields=[ 118 | ( 119 | "id", 120 | models.BigAutoField( 121 | auto_created=True, 122 | primary_key=True, 123 | serialize=False, 124 | verbose_name="ID", 125 | ), 126 | ), 127 | ( 128 | "avatar", 129 | models.ImageField( 130 | blank=True, 131 | default="default-user.jpg", 132 | null=True, 133 | upload_to="user_avatars", 134 | ), 135 | ), 136 | ("bio", models.TextField(blank=True, null=True)), 137 | ("country", models.CharField(blank=True, max_length=100, null=True)), 138 | ( 139 | "user", 140 | models.OneToOneField( 141 | on_delete=django.db.models.deletion.CASCADE, 142 | to=settings.AUTH_USER_MODEL, 143 | ), 144 | ), 145 | ], 146 | ), 147 | ] 148 | -------------------------------------------------------------------------------- /backend/userauths/migrations/0002_remove_customuser_full_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2024-08-23 04:09 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("userauths", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="customuser", 15 | name="full_name", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/userauths/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalvincalimag/django-nextjs-jwt-starter/15f01b62eac89c2979a1c978fee764784e636d5d/backend/userauths/migrations/__init__.py -------------------------------------------------------------------------------- /backend/userauths/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser 3 | from django.db.models.signals import post_save 4 | from django.dispatch import receiver 5 | 6 | class CustomUser(AbstractUser): 7 | email = models.EmailField(unique=True) 8 | username = models.CharField(unique=True, max_length=100) 9 | 10 | USERNAME_FIELD = 'email' 11 | REQUIRED_FIELDS = ['username'] 12 | 13 | def save(self, *args, **kwargs): 14 | if not self.username: 15 | self.username = self.email.split('@')[0] 16 | super().save(*args, **kwargs) 17 | 18 | def __str__(self): 19 | return self.email 20 | 21 | class Profile(models.Model): 22 | user = models.OneToOneField(CustomUser, on_delete=models.CASCADE) 23 | avatar = models.ImageField(upload_to='user_avatars', default='default-user.jpg', null=True, blank=True) 24 | bio = models.TextField(null=True, blank=True) 25 | country = models.CharField(max_length=100, null=True, blank=True) 26 | 27 | def __str__(self): 28 | return f"{self.user.username}'s Profile" 29 | 30 | @receiver(post_save, sender=CustomUser) 31 | def create_or_update_user_profile(sender, instance, created, **kwargs): 32 | if created: 33 | Profile.objects.create(user=instance) 34 | else: 35 | instance.profile.save() -------------------------------------------------------------------------------- /backend/userauths/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/userauths/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /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 | "@babel/core": "^7.25.2", 13 | "jwt-decode": "^4.0.0", 14 | "next": "^14.2.6", 15 | "react": "^18", 16 | "react-dom": "^18" 17 | }, 18 | "devDependencies": { 19 | "@babel/preset-react": "^7.24.7", 20 | "@types/node": "^20", 21 | "@types/react": "^18", 22 | "@types/react-dom": "^18", 23 | "eslint": "^8", 24 | "eslint-config-next": "^14.2.6", 25 | "postcss": "^8", 26 | "tailwindcss": "^3.4.1", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalvincalimag/django-nextjs-jwt-starter/15f01b62eac89c2979a1c978fee764784e636d5d/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --navbar-height: 65px; 7 | } -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import Header from '../components/Header'; 5 | import { AuthProvider } from '../context/AuthContext' 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Create Next App", 11 | description: "Generated by create next app", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 |
24 | {children} 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/app/login/page.js: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState } from 'react' 3 | import { useAuth } from '../../context/AuthContext' 4 | import Link from 'next/link' 5 | 6 | export default function Login() { 7 | const [credentials, setCredentials] = useState({ email: '', password: '' }) 8 | const { loginUser } = useAuth() 9 | 10 | const handleSubmit = (e) => { 11 | e.preventDefault() 12 | loginUser(credentials) 13 | } 14 | 15 | return ( 16 |
17 |
18 |
19 |

20 | Sign in to your account 21 |

22 |
23 |
24 |
25 |
26 | 29 | setCredentials({ ...credentials, email: e.target.value })} 39 | /> 40 |
41 |
42 | 45 | setCredentials({ ...credentials, password: e.target.value })} 55 | /> 56 |
57 |
58 | 59 |
60 | 66 |
67 |
68 |
69 | 70 | Don't have an account yet? Sign up 71 | 72 |
73 |
74 |
75 | ) 76 | } -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useAuth } from '../context/AuthContext' 3 | import { useEffect } from 'react' 4 | import { useRouter } from 'next/navigation' 5 | 6 | export default function Home() { 7 | const { user, loading } = useAuth() 8 | const router = useRouter() 9 | 10 | useEffect(() => { 11 | if (!loading && !user) { 12 | router.push('/login') 13 | } 14 | }, [user, loading, router]) 15 | 16 | if (loading) return
Loading...
17 | 18 | return ( 19 | user ? ( 20 |
21 |

You are logged in to the homepage.

22 |

Welcome, {user.username}!

23 |
24 | ) : ( 25 |
26 |

You are not logged in, redirecting...

27 |
28 | ) 29 | ); 30 | } -------------------------------------------------------------------------------- /frontend/src/app/register/page.js: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import { useRouter } from 'next/navigation' 5 | import Link from 'next/link' 6 | import { useAuth } from '../../context/AuthContext' 7 | 8 | export default function Register() { 9 | const [formData, setFormData] = useState({ 10 | username: '', 11 | email: '', 12 | password: '', 13 | password2: '' 14 | }) 15 | const [error, setError] = useState('') 16 | 17 | const { registerUser } = useAuth() 18 | const router = useRouter() 19 | 20 | const handleChange = (e) => { 21 | const { name, value } = e.target 22 | setFormData(prevState => ({ 23 | ...prevState, 24 | [name]: value 25 | })) 26 | } 27 | 28 | const handleSubmit = async (e) => { 29 | e.preventDefault() 30 | setError('') 31 | 32 | const result = await registerUser(formData) 33 | 34 | if (result.success) { 35 | router.push('/login') 36 | } else { 37 | setError(result.errors.email || result.errors.password || 'Registration failed') 38 | } 39 | } 40 | 41 | return ( 42 |
43 |
44 |
45 |

46 | Create your account 47 |

48 |
49 |
50 | 51 |
52 |
53 | 56 | 66 |
67 |
68 | 71 | 82 |
83 |
84 | 87 | 98 |
99 |
100 | 103 | 114 |
115 |
116 | 117 | {error && ( 118 |
119 | {error} 120 |
121 | )} 122 | 123 |
124 | 130 |
131 |
132 |
133 | 134 | Already have an account? Sign in 135 | 136 |
137 |
138 |
139 | ) 140 | } -------------------------------------------------------------------------------- /frontend/src/components/Header.js: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from 'next/link' 3 | import { useAuth } from '../context/AuthContext' 4 | 5 | const Header = () => { 6 | const { user, logoutUser } = useAuth() 7 | 8 | return ( 9 |
10 | 37 |
38 | ) 39 | } 40 | 41 | export default Header -------------------------------------------------------------------------------- /frontend/src/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { createContext, useState, useContext, useEffect, useCallback } from 'react' 3 | import { useRouter } from 'next/navigation' 4 | import {jwtDecode} from 'jwt-decode' 5 | 6 | const AuthContext = createContext() 7 | 8 | export const AuthProvider = ({ children }) => { 9 | const [user, setUser] = useState(null) 10 | const [authTokens, setAuthTokens] = useState(null) 11 | const [loading, setLoading] = useState(true) 12 | const router = useRouter() 13 | 14 | const updateToken = useCallback(async () => { 15 | if (!authTokens?.refresh) { 16 | setLoading(false) 17 | return 18 | } 19 | 20 | try { 21 | const response = await fetch('http://127.0.0.1:8000/api/token/refresh/', { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json' 25 | }, 26 | body: JSON.stringify({ refresh: authTokens.refresh }) 27 | }) 28 | 29 | const data = await response.json() 30 | if (response.ok) { 31 | setAuthTokens(data) 32 | setUser(jwtDecode(data.access)) 33 | localStorage.setItem('authTokens', JSON.stringify(data)) 34 | document.cookie = `auth_token=${data.access}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Strict; Secure` 35 | } else { 36 | logoutUser() 37 | } 38 | } catch (error) { 39 | console.error('Token refresh error:', error) 40 | logoutUser() 41 | } finally { 42 | setLoading(false) 43 | } 44 | }, [authTokens]) 45 | 46 | const registerUser = async (userData) => { 47 | try { 48 | const response = await fetch('http://127.0.0.1:8000/api/register/', { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify(userData), 54 | }) 55 | 56 | if (response.ok) { 57 | return { success: true } 58 | } else { 59 | const data = await response.json() 60 | return { success: false, errors: data } 61 | } 62 | } catch (error) { 63 | console.error('Registration error:', error) 64 | return { success: false, errors: { non_field_errors: ['An unexpected error occurred'] } } 65 | } 66 | } 67 | 68 | const loginUser = async (credentials) => { 69 | try { 70 | const response = await fetch('http://127.0.0.1:8000/api/token/', { 71 | method: 'POST', 72 | headers: { 73 | 'Content-Type': 'application/json' 74 | }, 75 | body: JSON.stringify(credentials) 76 | }) 77 | 78 | const data = await response.json() 79 | 80 | if (response.ok) { 81 | setAuthTokens(data) 82 | setUser(jwtDecode(data.access)) 83 | localStorage.setItem('authTokens', JSON.stringify(data)) 84 | document.cookie = `auth_token=${data.access}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Strict; Secure` 85 | router.push('/') 86 | } else { 87 | throw new Error(data.detail || 'Login failed') 88 | } 89 | } catch (error) { 90 | alert(error.message) 91 | } 92 | } 93 | 94 | const logoutUser = useCallback(() => { 95 | localStorage.removeItem('authTokens') 96 | document.cookie = 'auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' 97 | setAuthTokens(null) 98 | setUser(null) 99 | router.push('/login') 100 | }, [router]) 101 | 102 | useEffect(() => { 103 | const storedTokens = localStorage.getItem('authTokens') 104 | if (storedTokens) { 105 | const tokens = JSON.parse(storedTokens) 106 | setAuthTokens(tokens) 107 | setUser(jwtDecode(tokens.access)) 108 | } 109 | setLoading(false) 110 | }, []) 111 | 112 | useEffect(() => { 113 | const REFRESH_INTERVAL = 1000 * 60 * 4 // 4 minutes 114 | let interval 115 | 116 | if (authTokens) { 117 | interval = setInterval(() => { 118 | updateToken() 119 | }, REFRESH_INTERVAL) 120 | } 121 | 122 | return () => clearInterval(interval) 123 | }, [authTokens, updateToken]) 124 | 125 | const contextData = { 126 | user, 127 | authTokens, 128 | registerUser, 129 | loginUser, 130 | logoutUser, 131 | loading 132 | } 133 | 134 | return ( 135 | 136 | {children} 137 | 138 | ) 139 | } 140 | 141 | export const useAuth = () => useContext(AuthContext) -------------------------------------------------------------------------------- /frontend/src/middleware.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | 3 | export function middleware(request) { 4 | const isAuthenticated = request.cookies.get('auth_token') 5 | 6 | if (!isAuthenticated && !['/login', '/register'].includes(request.nextUrl.pathname)) { 7 | return NextResponse.redirect(new URL('/login', request.url)) 8 | } 9 | 10 | return NextResponse.next() 11 | } 12 | 13 | export const config = { 14 | matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], 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 | --------------------------------------------------------------------------------