├── README.md ├── backend ├── .gitignore ├── account │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── http_only_auth │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── requirements.txt └── frontend ├── .env ├── .eslintrc ├── .gitignore ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── vercel.svg └── src ├── actions ├── auth.js └── types.js ├── components └── Navbar.js ├── config └── index.js ├── hocs └── Layout.js ├── pages ├── _app.js ├── api │ └── account │ │ ├── login.js │ │ ├── logout.js │ │ ├── refresh.js │ │ ├── register.js │ │ ├── user.js │ │ └── verify.js ├── dashboard.js ├── index.js ├── login.js └── register.js ├── reducers ├── auth.js └── index.js └── store.js /README.md: -------------------------------------------------------------------------------- 1 | # Next.js & Django JWT Auth with httpOnly Cookies 2 | 3 | This is a project that demonstrates how you could implement json web token authentication where you store your json web tokens in an httpOnly cookie to prevent JavaScript from having access to your credentials. 4 | 5 | In order to test out this project, follow these steps: 6 | 7 | - clone the repository 8 | - in the frontend folder, run: npm install, this will install the required frontend packages 9 | - in the frontend folder, run: npm run dev, this will run your Next.js frontend 10 | - in the backend folder, run: python3 -m venv venv 11 | - then activate the virtual environment: source venv/bin/activate (MacOS) or venv\Scripts\activate (Windows) 12 | - in the backend folder, run: pip install -r requirements.txt 13 | - in the backend folder, run: python manage.py migrate 14 | - in the backend folder, run: python manage.py runserver 15 | 16 | Then under backend/auth_system/settings.py: 17 | 18 | - under DATABASES, set the PASSWORD field to your database password 19 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/django 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=django 4 | 5 | ### Django ### 6 | *.log 7 | *.pot 8 | *.pyc 9 | __pycache__/ 10 | local_settings.py 11 | db.sqlite3 12 | db.sqlite3-journal 13 | media 14 | 15 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 16 | # in your Git repository. Update and uncomment the following line accordingly. 17 | # /staticfiles/ 18 | 19 | ### Django.Python Stack ### 20 | # Byte-compiled / optimized / DLL files 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | *.py,cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | cover/ 71 | 72 | # Translations 73 | *.mo 74 | 75 | # Django stuff: 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | .pybuilder/ 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | # For a library or package, you might want to ignore these files since the code is 100 | # intended to run in multiple environments; otherwise, check them in: 101 | # .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 111 | __pypackages__/ 112 | 113 | # Celery stuff 114 | celerybeat-schedule 115 | celerybeat.pid 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .venv 123 | env/ 124 | venv/ 125 | ENV/ 126 | env.bak/ 127 | venv.bak/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ 141 | .dmypy.json 142 | dmypy.json 143 | 144 | # Pyre type checker 145 | .pyre/ 146 | 147 | # pytype static type analyzer 148 | .pytype/ 149 | 150 | # Cython debug symbols 151 | cython_debug/ 152 | 153 | # End of https://www.toptal.com/developers/gitignore/api/django -------------------------------------------------------------------------------- /backend/account/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/httponly-auth/115afd119c3399be6f931e934d198be1eba3759d/backend/account/__init__.py -------------------------------------------------------------------------------- /backend/account/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'account' 7 | -------------------------------------------------------------------------------- /backend/account/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/httponly-auth/115afd119c3399be6f931e934d198be1eba3759d/backend/account/migrations/__init__.py -------------------------------------------------------------------------------- /backend/account/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /backend/account/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class UserSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = User 8 | fields = ('first_name', 'last_name', 'username', ) 9 | -------------------------------------------------------------------------------- /backend/account/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/account/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import RegisterView, LoadUserView 3 | 4 | 5 | urlpatterns = [ 6 | path('register', RegisterView.as_view()), 7 | path('user', LoadUserView.as_view()), 8 | ] 9 | -------------------------------------------------------------------------------- /backend/account/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework.response import Response 3 | from rest_framework import permissions, status 4 | from django.contrib.auth.models import User 5 | from .serializers import UserSerializer 6 | 7 | 8 | class RegisterView(APIView): 9 | permission_classes = (permissions.AllowAny, ) 10 | 11 | def post(self, request): 12 | try: 13 | data = request.data 14 | 15 | first_name = data['first_name'] 16 | last_name = data['last_name'] 17 | username = data['username'] 18 | password = data['password'] 19 | re_password = data['re_password'] 20 | 21 | if password == re_password: 22 | if len(password) >= 8: 23 | if not User.objects.filter(username=username).exists(): 24 | user = User.objects.create_user( 25 | first_name=first_name, 26 | last_name=last_name, 27 | username=username, 28 | password=password, 29 | ) 30 | 31 | user.save() 32 | 33 | if User.objects.filter(username=username).exists(): 34 | return Response( 35 | {'success': 'Account created successfully'}, 36 | status=status.HTTP_201_CREATED 37 | ) 38 | else: 39 | return Response( 40 | {'error': 'Something went wrong when trying to create account'}, 41 | status=status.HTTP_500_INTERNAL_SERVER_ERROR 42 | ) 43 | else: 44 | return Response( 45 | {'error': 'Username already exists'}, 46 | status=status.HTTP_400_BAD_REQUEST 47 | ) 48 | else: 49 | return Response( 50 | {'error': 'Password must be at least 8 characters in length'}, 51 | status=status.HTTP_400_BAD_REQUEST 52 | ) 53 | else: 54 | return Response( 55 | {'error': 'Passwords do not match'}, 56 | status=status.HTTP_400_BAD_REQUEST 57 | ) 58 | except: 59 | return Response( 60 | {'error': 'Something went wrong when trying to register account'}, 61 | status=status.HTTP_500_INTERNAL_SERVER_ERROR 62 | ) 63 | 64 | class LoadUserView(APIView): 65 | def get(self, request, format=None): 66 | try: 67 | user = request.user 68 | user = UserSerializer(user) 69 | 70 | return Response( 71 | {'user': user.data}, 72 | status=status.HTTP_200_OK 73 | ) 74 | except: 75 | return Response( 76 | {'error': 'Something went wrong when trying to load user'}, 77 | status=status.HTTP_500_INTERNAL_SERVER_ERROR 78 | ) 79 | -------------------------------------------------------------------------------- /backend/http_only_auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/httponly-auth/115afd119c3399be6f931e934d198be1eba3759d/backend/http_only_auth/__init__.py -------------------------------------------------------------------------------- /backend/http_only_auth/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for http_only_auth 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/3.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', 'http_only_auth.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/http_only_auth/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for http_only_auth project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | from datetime import timedelta 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-b-983e_%hxqb!#c^ls5w0g20m!hu*fxatlg8#0@8q8_l4*)7qe' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'rest_framework', 42 | 'corsheaders', 43 | 'rest_framework_simplejwt.token_blacklist', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'corsheaders.middleware.CorsMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'http_only_auth.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'http_only_auth.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.postgresql', 84 | 'NAME': 'http_only_auth', 85 | 'PASSWORD': 'password123', 86 | 'USER': 'postgres', 87 | 'HOST': 'localhost' 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | 130 | REST_FRAMEWORK = { 131 | 'DEFAULT_PERMISSION_CLASSES': [ 132 | 'rest_framework.permissions.IsAuthenticated', 133 | ], 134 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 135 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 136 | ) 137 | } 138 | 139 | SIMPLE_JWT = { 140 | 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), 141 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), 142 | 'ROTATE_REFRESH_TOKENS': True, 143 | 'BLACKLIST_AFTER_ROTATION': True, 144 | 'AUTH_HEADER_TYPES': ('Bearer', ), 145 | 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken', ), 146 | } 147 | 148 | CORS_ALLOWED_ORIGINS = [ 149 | 'http://localhost:3000', 150 | ] 151 | 152 | # Default primary key field type 153 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 154 | 155 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 156 | -------------------------------------------------------------------------------- /backend/http_only_auth/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView 4 | 5 | urlpatterns = [ 6 | path('api/token/', TokenObtainPairView.as_view()), 7 | path('api/token/refresh/', TokenRefreshView.as_view()), 8 | path('api/token/verify/', TokenVerifyView.as_view()), 9 | path('api/account/', include('account.urls')), 10 | path('admin/', admin.site.urls), 11 | ] 12 | -------------------------------------------------------------------------------- /backend/http_only_auth/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for http_only_auth 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/3.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', 'http_only_auth.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', 'http_only_auth.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: -------------------------------------------------------------------------------- 1 | Django==3.2.5 2 | django-cors-headers==3.7.0 3 | djangorestframework==3.12.4 4 | djangorestframework-simplejwt==4.7.2 5 | psycopg2==2.9.1 6 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV = 'development' 2 | NEXT_PUBLIC_API_URL = 'http://localhost:8000' -------------------------------------------------------------------------------- /frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /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 | "cookie": "^0.4.1", 13 | "next": "11.0.1", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2", 16 | "react-loader-spinner": "^4.0.0", 17 | "react-redux": "^7.2.4", 18 | "redux": "^4.1.0", 19 | "redux-devtools-extension": "^2.13.9", 20 | "redux-thunk": "^2.3.0" 21 | }, 22 | "devDependencies": { 23 | "eslint": "7.31.0", 24 | "eslint-config-next": "11.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/httponly-auth/115afd119c3399be6f931e934d198be1eba3759d/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_SUCCESS, 3 | REGISTER_FAIL, 4 | RESET_REGISTER_SUCCESS, 5 | LOGIN_SUCCESS, 6 | LOGIN_FAIL, 7 | LOGOUT_SUCCESS, 8 | LOGOUT_FAIL, 9 | LOAD_USER_SUCCESS, 10 | LOAD_USER_FAIL, 11 | AUTHENTICATED_SUCCESS, 12 | AUTHENTICATED_FAIL, 13 | REFRESH_SUCCESS, 14 | REFRESH_FAIL, 15 | SET_AUTH_LOADING, 16 | REMOVE_AUTH_LOADING, 17 | } from './types'; 18 | 19 | export const load_user = () => async dispatch => { 20 | try { 21 | const res = await fetch('/api/account/user', { 22 | method: 'GET', 23 | headers: { 24 | 'Accept': 'application/json' 25 | } 26 | }); 27 | 28 | const data = await res.json(); 29 | 30 | if (res.status === 200) { 31 | dispatch({ 32 | type: LOAD_USER_SUCCESS, 33 | payload: data 34 | }); 35 | } else { 36 | dispatch({ 37 | type: LOAD_USER_FAIL 38 | }); 39 | } 40 | } catch(err) { 41 | dispatch({ 42 | type: LOAD_USER_FAIL 43 | }); 44 | } 45 | }; 46 | 47 | export const check_auth_status = () => async dispatch => { 48 | try { 49 | const res = await fetch('/api/account/verify', { 50 | method: 'GET', 51 | headers: { 52 | 'Accept': 'application/json', 53 | } 54 | }); 55 | 56 | if (res.status === 200) { 57 | dispatch({ 58 | type: AUTHENTICATED_SUCCESS 59 | }); 60 | dispatch(load_user()); 61 | } else { 62 | dispatch({ 63 | type: AUTHENTICATED_FAIL 64 | }); 65 | } 66 | } catch(err) { 67 | dispatch({ 68 | type: AUTHENTICATED_FAIL 69 | }); 70 | } 71 | }; 72 | 73 | export const request_refresh = () => async dispatch => { 74 | try { 75 | const res = await fetch('/api/account/refresh', { 76 | method: 'GET', 77 | headers: { 78 | 'Accept': 'application/json', 79 | } 80 | }); 81 | 82 | if (res.status === 200) { 83 | dispatch({ 84 | type: REFRESH_SUCCESS 85 | }); 86 | dispatch(check_auth_status()); 87 | } else { 88 | dispatch({ 89 | type: REFRESH_FAIL 90 | }); 91 | } 92 | } catch(err) { 93 | dispatch({ 94 | type: REFRESH_FAIL 95 | }); 96 | } 97 | }; 98 | 99 | export const register = ( 100 | first_name, 101 | last_name, 102 | username, 103 | password, 104 | re_password 105 | ) => async dispatch => { 106 | const body = JSON.stringify({ 107 | first_name, 108 | last_name, 109 | username, 110 | password, 111 | re_password 112 | }); 113 | 114 | dispatch({ 115 | type: SET_AUTH_LOADING 116 | }); 117 | 118 | try { 119 | const res = await fetch('/api/account/register', { 120 | method: 'POST', 121 | headers: { 122 | 'Accept': 'application/json', 123 | 'Content-Type': 'application/json', 124 | }, 125 | body: body 126 | }); 127 | 128 | if (res.status === 201) { 129 | dispatch({ 130 | type: REGISTER_SUCCESS 131 | }); 132 | } else { 133 | dispatch({ 134 | type: REGISTER_FAIL 135 | }); 136 | } 137 | } catch(err) { 138 | dispatch({ 139 | type: REGISTER_FAIL 140 | }); 141 | } 142 | 143 | dispatch({ 144 | type: REMOVE_AUTH_LOADING 145 | }); 146 | }; 147 | 148 | export const reset_register_success = () => dispatch => { 149 | dispatch({ 150 | type: RESET_REGISTER_SUCCESS 151 | }); 152 | }; 153 | 154 | export const login = (username, password) => async dispatch => { 155 | const body = JSON.stringify({ 156 | username, 157 | password 158 | }); 159 | 160 | dispatch({ 161 | type: SET_AUTH_LOADING 162 | }); 163 | 164 | try { 165 | const res = await fetch('/api/account/login', { 166 | method: 'POST', 167 | headers: { 168 | 'Accept': 'application/json', 169 | 'Content-Type': 'application/json' 170 | }, 171 | body: body 172 | }); 173 | 174 | if (res.status === 200) { 175 | dispatch({ 176 | type: LOGIN_SUCCESS 177 | }); 178 | dispatch(load_user()); 179 | } else { 180 | dispatch({ 181 | type: LOGIN_FAIL 182 | }); 183 | } 184 | } catch(err) { 185 | dispatch({ 186 | type: LOGIN_FAIL 187 | }); 188 | } 189 | 190 | dispatch({ 191 | type: REMOVE_AUTH_LOADING 192 | }); 193 | }; 194 | 195 | export const logout = () => async dispatch => { 196 | try { 197 | const res = await fetch('/api/account/logout', { 198 | method: 'POST', 199 | headers: { 200 | 'Accept': 'application/json', 201 | } 202 | }); 203 | 204 | if (res.status === 200) { 205 | dispatch({ 206 | type: LOGOUT_SUCCESS 207 | }); 208 | } else { 209 | dispatch({ 210 | type: LOGOUT_FAIL 211 | }); 212 | } 213 | } catch(err) { 214 | dispatch({ 215 | type: LOGOUT_FAIL 216 | }); 217 | } 218 | }; 219 | -------------------------------------------------------------------------------- /frontend/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const REGISTER_SUCCESS = 'REGISTER_SUCCESS'; 2 | export const REGISTER_FAIL = 'REGISTER_FAIL'; 3 | export const RESET_REGISTER_SUCCESS = 'RESET_REGISTER_SUCCESS'; 4 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; 5 | export const LOGIN_FAIL = 'LOGIN_FAIL'; 6 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; 7 | export const LOGOUT_FAIL = 'LOGOUT_FAIL'; 8 | export const LOAD_USER_SUCCESS = 'LOAD_USER_SUCCESS'; 9 | export const LOAD_USER_FAIL = 'LOAD_USER_FAIL'; 10 | export const AUTHENTICATED_SUCCESS = 'AUTHENTICATED_SUCCESS'; 11 | export const AUTHENTICATED_FAIL = 'AUTHENTICATED_FAIL'; 12 | export const REFRESH_SUCCESS = 'REFRESH_SUCCESS'; 13 | export const REFRESH_FAIL = 'REFRESH_FAIL'; 14 | export const SET_AUTH_LOADING = 'SET_AUTH_LOADING'; 15 | export const REMOVE_AUTH_LOADING = 'REMOVE_AUTH_LOADING'; 16 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useRouter } from 'next/router'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { logout } from '../actions/auth'; 5 | 6 | const navbar = () => { 7 | const dispatch = useDispatch(); 8 | const router = useRouter(); 9 | 10 | const isAuthenticated = useSelector(state => state.auth.isAuthenticated); 11 | 12 | const logoutHandler = () => { 13 | if (dispatch && dispatch !== null && dispatch !== undefined) 14 | dispatch(logout()); 15 | }; 16 | 17 | const authLinks = ( 18 | <> 19 |
  • 20 | 21 | 25 | Dashboard 26 | 27 | 28 |
  • 29 |
  • 30 | 35 | Logout 36 | 37 |
  • 38 | 39 | ); 40 | 41 | const guestLinks = ( 42 | <> 43 |
  • 44 | 45 | 49 | Register 50 | 51 | 52 |
  • 53 |
  • 54 | 55 | 59 | Login 60 | 61 | 62 |
  • 63 | 64 | ); 65 | 66 | return ( 67 | 104 | ); 105 | }; 106 | 107 | export default navbar; 108 | -------------------------------------------------------------------------------- /frontend/src/config/index.js: -------------------------------------------------------------------------------- 1 | export const API_URL = process.env.NEXT_PUBLIC_API_URL; -------------------------------------------------------------------------------- /frontend/src/hocs/Layout.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { request_refresh } from '../actions/auth'; 4 | import Head from 'next/head'; 5 | import Navbar from '../components/Navbar'; 6 | 7 | const Layout = ({ title, content, children }) => { 8 | const dispatch = useDispatch(); 9 | 10 | useEffect(() => { 11 | if (dispatch && dispatch !== null && dispatch !== undefined) 12 | dispatch(request_refresh()); 13 | }, [dispatch]); 14 | 15 | return ( 16 | <> 17 | 18 | {title} 19 | 20 | 21 | 22 |
    23 | {children} 24 |
    25 | 26 | ); 27 | }; 28 | 29 | Layout.defaultProps = { 30 | title: 'httpOnly Auth', 31 | content: 'Tutorial for showing you how to use httpOnly cookies for storing json web tokens' 32 | } 33 | 34 | export default Layout; 35 | -------------------------------------------------------------------------------- /frontend/src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Provider } from 'react-redux'; 3 | import { useStore } from '../store'; 4 | 5 | const App = ({ Component, pageProps }) => { 6 | const store = useStore(pageProps.initialReduxState); 7 | 8 | return ( 9 | 10 | 11 | HTTPOnly Auth 12 | 13 | 19 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/login.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | import { API_URL } from '../../../config/index'; 3 | 4 | 5 | export default async (req, res) => { 6 | if (req.method === 'POST') { 7 | const { username, password } = req.body; 8 | 9 | const body = JSON.stringify({ 10 | username, 11 | password 12 | }); 13 | 14 | try { 15 | const apiRes = await fetch(`${API_URL}/api/token/`, { 16 | method: 'POST', 17 | headers: { 18 | 'Accept': 'application/json', 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: body 22 | }); 23 | 24 | const data = await apiRes.json(); 25 | 26 | if (apiRes.status === 200) { 27 | res.setHeader('Set-Cookie', [ 28 | cookie.serialize( 29 | 'access', data.access, { 30 | httpOnly: true, 31 | secure: process.env.NODE_ENV !== 'development', 32 | maxAge: 60 * 30, 33 | sameSite: 'strict', 34 | path: '/api/' 35 | } 36 | ), 37 | cookie.serialize( 38 | 'refresh', data.refresh, { 39 | httpOnly: true, 40 | secure: process.env.NODE_ENV !== 'development', 41 | maxAge: 60 * 60 * 24, 42 | sameSite: 'strict', 43 | path: '/api/' 44 | } 45 | ) 46 | ]); 47 | 48 | return res.status(200).json({ 49 | success: 'Logged in successfully' 50 | }); 51 | } else { 52 | return res.status(apiRes.status).json({ 53 | error: 'Authentication failed' 54 | }); 55 | } 56 | } catch(err) { 57 | return res.status(500).json({ 58 | error: 'Something went wrong when authenticating' 59 | }); 60 | } 61 | } else { 62 | res.setHeader('Allow', ['POST']); 63 | return res.status(405).json({ error: `Method ${req.method} now allowed` }); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/logout.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | 3 | export default async (req, res) => { 4 | if (req.method === 'POST') { 5 | res.setHeader('Set-Cookie', [ 6 | cookie.serialize( 7 | 'access', '', { 8 | httpOnly: true, 9 | secure: process.env.NODE_ENV !== 'development', 10 | expires: new Date(0), 11 | sameSite: 'strict', 12 | path: '/api/' 13 | } 14 | ), 15 | cookie.serialize( 16 | 'refresh', '', { 17 | httpOnly: true, 18 | secure: process.env.NODE_ENV !== 'development', 19 | expires: new Date(0), 20 | sameSite: 'strict', 21 | path: '/api/' 22 | } 23 | ) 24 | ]); 25 | 26 | return res.status(200).json({ 27 | success: 'Successfully logged out' 28 | }); 29 | } else { 30 | res.setHeader('Allow', ['POST']); 31 | return res.status(405).json({ 32 | error: `Method ${req.method} now allowed` 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/refresh.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | import { API_URL } from '../../../config/index'; 3 | 4 | export default async (req, res) => { 5 | if (req.method === 'GET') { 6 | const cookies = cookie.parse(req.headers.cookie ?? ''); 7 | const refresh = cookies.refresh ?? false; 8 | 9 | if (refresh === false) { 10 | return res.status(401).json({ 11 | error: 'User unauthorized to make this request' 12 | }); 13 | } 14 | 15 | const body = JSON.stringify({ 16 | refresh 17 | }); 18 | 19 | try { 20 | const apiRes = await fetch(`${API_URL}/api/token/refresh/`, { 21 | method: 'POST', 22 | headers: { 23 | 'Accept': 'application/json', 24 | 'Content-Type': 'application/json', 25 | }, 26 | body: body 27 | }); 28 | 29 | const data = await apiRes.json(); 30 | 31 | if (apiRes.status === 200) { 32 | res.setHeader('Set-Cookie', [ 33 | cookie.serialize( 34 | 'access', data.access, { 35 | httpOnly: true, 36 | secure: process.env.NODE_ENV !== 'development', 37 | maxAge: 60 * 30, 38 | sameSite: 'strict', 39 | path: '/api/' 40 | } 41 | ), 42 | cookie.serialize( 43 | 'refresh', data.refresh, { 44 | httpOnly: true, 45 | secure: process.env.NODE_ENV !== 'development', 46 | maxAge: 60 * 60 * 24, 47 | sameSite: 'strict', 48 | path: '/api/' 49 | } 50 | ) 51 | ]); 52 | 53 | return res.status(200).json({ 54 | success: 'Refresh request successful' 55 | }); 56 | } else { 57 | return res.status(apiRes.status).json({ 58 | error: 'Failed to fulfill refresh request' 59 | }); 60 | } 61 | } catch(err) { 62 | return res.status(500).json({ 63 | error: 'Something went wrong when trying to fulfill refresh request' 64 | }); 65 | } 66 | } else { 67 | res.setHeader('Allow', ['GET']); 68 | return res.status(405).json( 69 | { error: `Method ${req.method} not allowed` } 70 | ) 71 | } 72 | }; -------------------------------------------------------------------------------- /frontend/src/pages/api/account/register.js: -------------------------------------------------------------------------------- 1 | import { API_URL } from '../../../config/index'; 2 | 3 | export default async (req, res) => { 4 | if (req.method === 'POST') { 5 | const { 6 | first_name, 7 | last_name, 8 | username, 9 | password, 10 | re_password 11 | } = req.body; 12 | 13 | const body = JSON.stringify({ 14 | first_name, 15 | last_name, 16 | username, 17 | password, 18 | re_password 19 | }); 20 | 21 | try { 22 | const apiRes = await fetch(`${API_URL}/api/account/register`, { 23 | method: 'POST', 24 | headers: { 25 | 'Accept': 'application/json', 26 | 'Content-Type': 'application/json' 27 | }, 28 | body: body 29 | }); 30 | 31 | const data = await apiRes.json(); 32 | 33 | if (apiRes.status === 201) { 34 | return res.status(201).json({ success: data.success }); 35 | } else { 36 | return res.status(apiRes.status).json({ 37 | error: data.error 38 | }); 39 | } 40 | } catch(err) { 41 | return res.status(500).json({ 42 | error: 'Something went wrong when registering for an account' 43 | }); 44 | } 45 | } else { 46 | res.setHeader('Allow', ['POST']); 47 | return res.status(405).json({ 'error': `Method ${req.method} not allowed`}); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/user.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | import { API_URL } from '../../../config/index'; 3 | 4 | export default async (req, res) => { 5 | if (req.method === 'GET') { 6 | const cookies = cookie.parse(req.headers.cookie ?? ''); 7 | const access = cookies.access ?? false; 8 | 9 | if (access === false) { 10 | return res.status(401).json({ 11 | error: 'User unauthorized to make this request' 12 | }); 13 | } 14 | 15 | try { 16 | const apiRes = await fetch(`${API_URL}/api/account/user`, { 17 | method: 'GET', 18 | headers: { 19 | 'Accept': 'application/json', 20 | 'Authorization': `Bearer ${access}` 21 | } 22 | }); 23 | const data = await apiRes.json(); 24 | 25 | if (apiRes.status === 200) { 26 | return res.status(200).json({ 27 | user: data.user 28 | }); 29 | } else { 30 | return res.status(apiRes.status).json({ 31 | error: data.error 32 | }); 33 | } 34 | } catch(err) { 35 | return res.status(500).json({ 36 | error: 'Something went wrong when retrieving user' 37 | }); 38 | } 39 | } else { 40 | res.setHeader('Allow', ['GET']); 41 | return res.status(405).json({ 42 | error: `Method ${req.method} not allowed` 43 | }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/verify.js: -------------------------------------------------------------------------------- 1 | import { API_URL } from '../../../config/index'; 2 | import cookie from 'cookie'; 3 | 4 | export default async (req, res) => { 5 | if (req.method === 'GET') { 6 | const cookies = cookie.parse(req.headers.cookie ?? ''); 7 | const access = cookies.access ?? false; 8 | 9 | if (access === false) { 10 | return res.status(403).json({ 11 | error: 'User forbidden from making the request' 12 | }); 13 | } 14 | 15 | const body = JSON.stringify({ 16 | token: access 17 | }); 18 | 19 | try { 20 | const apiRes = await fetch(`${API_URL}/api/token/verify/`, { 21 | method: 'POST', 22 | headers: { 23 | 'Accept': 'application/json', 24 | 'Content-Type': 'application/json', 25 | }, 26 | body: body 27 | }); 28 | 29 | if (apiRes.status === 200) { 30 | return res.status(200).json({ success: 'Authenticated successfully' }); 31 | } else { 32 | return res.status(apiRes.status).json({ 33 | error: 'Failed to authenticate' 34 | }); 35 | } 36 | } catch(err) { 37 | return res.status(500).json({ 38 | error: 'Something went wrong when trying to authenticate' 39 | }); 40 | } 41 | } else { 42 | res.setHeader('Allow', ['GET']); 43 | return res.status(405).json({ error: `Method ${req.method} not allowed` }); 44 | } 45 | }; -------------------------------------------------------------------------------- /frontend/src/pages/dashboard.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useSelector } from 'react-redux'; 3 | import Layout from '../hocs/Layout'; 4 | 5 | const Dashboard = () => { 6 | const router = useRouter(); 7 | 8 | const isAuthenticated = useSelector(state => state.auth.isAuthenticated); 9 | const user = useSelector(state => state.auth.user); 10 | const loading = useSelector(state => state.auth.loading); 11 | 12 | if (typeof window !== 'undefined' && !loading && !isAuthenticated) 13 | router.push('/login'); 14 | 15 | return ( 16 | 20 |
    21 |
    22 |

    23 | User Dashboard 24 |

    25 |

    26 | Welcome {user !== null && user.first_name} to the httpOnly Auth Tutorial Site! 27 |

    28 |
    29 |
    30 |
    31 | ); 32 | }; 33 | 34 | export default Dashboard; 35 | -------------------------------------------------------------------------------- /frontend/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Layout from '../hocs/Layout'; 2 | 3 | const homePage = () => ( 4 | 8 |
    9 |
    10 |

    Home Page

    11 |

    12 | Welcome to the httpOnly Auth Tutorial Site! 13 |

    14 |
    15 |
    16 |
    17 | ); 18 | 19 | export default homePage; 20 | -------------------------------------------------------------------------------- /frontend/src/pages/login.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { useRouter } from 'next/router'; 4 | import { login, reset_register_success } from '../actions/auth'; 5 | import Layout from '../hocs/Layout'; 6 | import Loader from 'react-loader-spinner'; 7 | 8 | const LoginPage = () => { 9 | const dispatch = useDispatch(); 10 | const router = useRouter(); 11 | const isAuthenticated = useSelector(state => state.auth.isAuthenticated); 12 | const loading = useSelector(state => state.auth.loading); 13 | 14 | const [formData, setFormData] = useState({ 15 | username: '', 16 | password: '', 17 | }); 18 | 19 | const { 20 | username, 21 | password, 22 | } = formData; 23 | 24 | useEffect(() => { 25 | if (dispatch && dispatch !== null && dispatch !== undefined) 26 | dispatch(reset_register_success()); 27 | }, [dispatch]); 28 | 29 | const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value }); 30 | 31 | const onSubmit = e => { 32 | e.preventDefault(); 33 | 34 | if (dispatch && dispatch !== null && dispatch !== undefined) 35 | dispatch(login(username, password)); 36 | }; 37 | 38 | if (typeof window !== 'undefined' && isAuthenticated) 39 | router.push('/dashboard'); 40 | 41 | return ( 42 | 46 |

    Login Page

    47 |
    48 |

    Log Into Your Account

    49 |
    50 | 53 | 62 |
    63 |
    64 | 67 | 77 |
    78 | { 79 | loading ? ( 80 |
    81 | 87 |
    88 | ) : ( 89 | 92 | ) 93 | } 94 |
    95 |
    96 | ); 97 | }; 98 | 99 | export default LoginPage; 100 | -------------------------------------------------------------------------------- /frontend/src/pages/register.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { useRouter } from 'next/router'; 4 | import { register } from '../actions/auth'; 5 | import Layout from '../hocs/Layout'; 6 | import Loader from 'react-loader-spinner'; 7 | import router from 'next/router'; 8 | 9 | const RegisterPage = () => { 10 | const dispatch = useDispatch(); 11 | const router = useRouter(); 12 | const register_success = useSelector(state => state.auth.register_success); 13 | const isAuthenticated = useSelector(state => state.auth.isAuthenticated); 14 | const loading = useSelector(state => state.auth.loading); 15 | 16 | const [formData, setFormData] = useState({ 17 | first_name: '', 18 | last_name: '', 19 | username: '', 20 | password: '', 21 | re_password: '', 22 | }); 23 | 24 | const { 25 | first_name, 26 | last_name, 27 | username, 28 | password, 29 | re_password 30 | } = formData; 31 | 32 | const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value }); 33 | 34 | const onSubmit = e => { 35 | e.preventDefault(); 36 | 37 | if (dispatch && dispatch !== null && dispatch !== undefined) 38 | dispatch(register(first_name, last_name, username, password, re_password)); 39 | }; 40 | 41 | if (typeof window !== 'undefined' && isAuthenticated) 42 | router.push('/dashboard'); 43 | if (register_success) 44 | router.push('/login'); 45 | 46 | return ( 47 | 51 |

    Register Page

    52 |
    53 |

    Create An Account

    54 |
    55 | 58 | 67 |
    68 |
    69 | 72 | 81 |
    82 |
    83 | 86 | 95 |
    96 |
    97 | 100 | 110 |
    111 |
    112 | 115 | 125 |
    126 | { 127 | loading ? ( 128 |
    129 | 135 |
    136 | ) : ( 137 | 140 | ) 141 | } 142 |
    143 |
    144 | ); 145 | }; 146 | 147 | export default RegisterPage; 148 | -------------------------------------------------------------------------------- /frontend/src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_SUCCESS, 3 | REGISTER_FAIL, 4 | RESET_REGISTER_SUCCESS, 5 | LOGIN_SUCCESS, 6 | LOGIN_FAIL, 7 | LOGOUT_SUCCESS, 8 | LOGOUT_FAIL, 9 | LOAD_USER_SUCCESS, 10 | LOAD_USER_FAIL, 11 | AUTHENTICATED_SUCCESS, 12 | AUTHENTICATED_FAIL, 13 | REFRESH_SUCCESS, 14 | REFRESH_FAIL, 15 | SET_AUTH_LOADING, 16 | REMOVE_AUTH_LOADING, 17 | } from '../actions/types'; 18 | 19 | const initialState = { 20 | user: null, 21 | isAuthenticated: false, 22 | loading: false, 23 | register_success: false 24 | }; 25 | 26 | const authReducer = (state = initialState, action) => { 27 | const { type, payload } = action; 28 | 29 | switch(type) { 30 | case REGISTER_SUCCESS: 31 | return { 32 | ...state, 33 | register_success: true 34 | } 35 | case REGISTER_FAIL: 36 | return { 37 | ...state, 38 | } 39 | case RESET_REGISTER_SUCCESS: 40 | return { 41 | ...state, 42 | register_success: false 43 | } 44 | case LOGIN_SUCCESS: 45 | return { 46 | ...state, 47 | isAuthenticated: true 48 | } 49 | case LOGIN_FAIL: 50 | return { 51 | ...state, 52 | isAuthenticated: false 53 | } 54 | case LOGOUT_SUCCESS: 55 | return { 56 | ...state, 57 | isAuthenticated: false, 58 | user: null 59 | } 60 | case LOGOUT_FAIL: 61 | return { 62 | ...state 63 | } 64 | case LOAD_USER_SUCCESS: 65 | return { 66 | ...state, 67 | user: payload.user 68 | } 69 | case LOAD_USER_FAIL: 70 | return { 71 | ...state, 72 | user: null 73 | } 74 | case AUTHENTICATED_SUCCESS: 75 | return { 76 | ...state, 77 | isAuthenticated: true 78 | } 79 | case AUTHENTICATED_FAIL: 80 | return { 81 | ...state, 82 | isAuthenticated: false, 83 | user: null 84 | } 85 | case REFRESH_SUCCESS: 86 | return { 87 | ...state, 88 | } 89 | case REFRESH_FAIL: 90 | return { 91 | ...state, 92 | isAuthenticated: false, 93 | user: null 94 | } 95 | case SET_AUTH_LOADING: 96 | return { 97 | ...state, 98 | loading: true 99 | } 100 | case REMOVE_AUTH_LOADING: 101 | return { 102 | ...state, 103 | loading: false 104 | } 105 | default: 106 | return state; 107 | }; 108 | }; 109 | 110 | export default authReducer; 111 | -------------------------------------------------------------------------------- /frontend/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import authReducer from './auth'; 3 | 4 | export default combineReducers({ 5 | auth: authReducer 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/src/store.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | import reducers from './reducers'; 6 | 7 | let store; 8 | 9 | function initStore(initialState) { 10 | return createStore( 11 | reducers, 12 | initialState, 13 | composeWithDevTools(applyMiddleware(thunkMiddleware)) 14 | ); 15 | }; 16 | 17 | export const initializeStore = (preloadedState) => { 18 | let _store = store ?? initStore(preloadedState); 19 | 20 | // After navigating to a page with an initial Redux state, merge that state 21 | // with the current state in the store, and create a new store 22 | if (preloadedState && store) { 23 | _store = initStore({ 24 | ...store.getState(), 25 | ...preloadedState, 26 | }) 27 | // Reset the current store 28 | store = undefined; 29 | } 30 | 31 | // For SSG and SSR always create a new store 32 | if (typeof window === 'undefined') return _store; 33 | // Create the store once in the client 34 | if (!store) store = _store; 35 | 36 | return _store; 37 | } 38 | 39 | export function useStore(initialState) { 40 | const store = useMemo(() => initializeStore(initialState), [initialState]); 41 | return store; 42 | }; 43 | --------------------------------------------------------------------------------