├── .gitignore ├── README.md ├── backend ├── README.md ├── manage.py ├── nextjsdrfauth │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── nextjsdrfbackend │ ├── __init__.py │ ├── asgi.py │ ├── secrets.example.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── posts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py └── requirements.txt └── client ├── .env.example ├── README.md ├── components └── Post.tsx ├── constants ├── HOCs.tsx ├── Hooks.tsx ├── Types.ts └── Utils.ts ├── next-env.d.ts ├── package.json ├── pages ├── _app.tsx ├── api │ └── auth │ │ └── [...nextauth].ts ├── index.tsx └── posts.tsx ├── public ├── favicon.ico └── vercel.svg ├── tsconfig.json └── types.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | secrets.py 3 | **/migrations 4 | db.sqlite3 5 | __pycache__ 6 | .vscode 7 | 8 | # dependencies 9 | client/node_modules 10 | .pnp 11 | .pnp.js 12 | 13 | # testing 14 | coverage 15 | 16 | # next.js 17 | client/.next 18 | client/out 19 | 20 | # production 21 | client/build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # local env files 33 | .env.local 34 | .env.development.local 35 | .env.test.local 36 | .env.production.local 37 | 38 | # lock files 39 | yarn.lock 40 | 41 | 42 | # vercel 43 | .vercel 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repository Archived 2 | 3 | The solution proposed here is currently outdated as of March 15, 2022. Next-Auth.js has had several revisions by now, [including a new v4.0 release](https://next-auth.js.org/getting-started/example) with a lot of changes to the API and new features. Please follow the updated documentation. 4 | 5 | # Next.js + NextAuth + Django Rest Framework Social Authentication Example 6 | 7 | This repository contains the code for a two-part article series that deals with connecting a Django Rest Framework backend with a Next.js + Next-Auth client with Social Authentication. In this example, we use OAuth with Google, but this can be extended to any arbitrary number of Providers. 8 | 9 | 10 | The articles: 11 | 12 | [Part 1](https://mahieyin-rahmun.medium.com/how-to-configure-social-authentication-in-a-next-js-next-auth-django-rest-framework-application-cb4c82be137). 13 | - The basics 14 | - Getting access token 15 | 16 | 17 | [Part 2](https://mahieyin-rahmun.medium.com/part-2-how-to-configure-social-authentication-in-a-next-js-183984761e97) 18 | - Using the refresh token to refresh access tokens 19 | - Pitfalls of useSession() hook and its workaround (as of Apr 30, 2021) 20 | - Custom HOC to reduce code repetition. 21 | 22 | The branches of this repository are named, accordingly, `part-1` and `part-2`. The `main` branch contains all the changes of both parts merged together into a single, working application. 23 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ## Django Rest Framework backed 2 | 3 | This folder contains the backend. You will notice that I have three lines commented out in the root `settings.py` file. That's because I was experimenting 4 | with `HttpOnly` cookie but forgot to exclude it from the repo. Those lines are not needed for this tutorial. 5 | 6 | Once you pull in the repository, make sure to create a `secrets.py` file inside the Django project folder and supply it with the necessary keys and values as shown 7 | in `secrets.example.py`. Then, make migrations and create a superuser. 8 | You also need to set up the social application in Django Admin panel with the OAuth provider's `client_id` and `client_secret`. 9 | -------------------------------------------------------------------------------- /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', 'nextjsdrfbackend.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/nextjsdrfauth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahieyin-rahmun/NextJsWithDRFExample/6a4140446ca05b4858c7fd74ca537efac5442178/backend/nextjsdrfauth/__init__.py -------------------------------------------------------------------------------- /backend/nextjsdrfauth/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import CustomUserModel 3 | 4 | # Register your models here. 5 | admin.site.register(CustomUserModel) -------------------------------------------------------------------------------- /backend/nextjsdrfauth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NextjsdrfauthConfig(AppConfig): 5 | name = 'nextjsdrfauth' 6 | -------------------------------------------------------------------------------- /backend/nextjsdrfauth/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin 3 | from uuid import uuid4 4 | 5 | # Create your models here. 6 | class CustomUserModelManager(BaseUserManager): 7 | def create_user(self, username, email, password=None): 8 | """ 9 | Creates a custom user with the given fields 10 | """ 11 | 12 | user = self.model( 13 | username = username, 14 | email = self.normalize_email(email), 15 | ) 16 | 17 | user.set_password(password) 18 | user.save(using = self._db) 19 | 20 | return user 21 | 22 | 23 | def create_superuser(self, username, email, password): 24 | user = self.create_user( 25 | username, 26 | email, 27 | password = password 28 | ) 29 | 30 | user.is_staff = True 31 | user.is_superuser = True 32 | user.save(using = self._db) 33 | 34 | return user 35 | 36 | 37 | class CustomUserModel(AbstractBaseUser, PermissionsMixin): 38 | userId = models.CharField(max_length = 16, default = uuid4, primary_key = True, editable = False) 39 | username = models.CharField(max_length = 16, unique = True, null = False, blank = False) 40 | email = models.EmailField(max_length = 100, unique = True, null = False, blank = False) 41 | 42 | USERNAME_FIELD = "username" 43 | REQUIRED_FIELDS = ["email"] 44 | 45 | active = models.BooleanField(default = True) 46 | 47 | is_staff = models.BooleanField(default = False) 48 | is_superuser = models.BooleanField(default = False) 49 | 50 | created_on = models.DateTimeField(auto_now_add = True, blank = True, null = True) 51 | updated_at = models.DateTimeField(auto_now = True) 52 | 53 | objects = CustomUserModelManager() 54 | 55 | class Meta: 56 | verbose_name = "Custom User" -------------------------------------------------------------------------------- /backend/nextjsdrfauth/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | from .models import CustomUserModel 3 | from django.conf import settings 4 | 5 | class CustomUserModelSerializer(ModelSerializer): 6 | class Meta: 7 | model = CustomUserModel 8 | fields = [ 9 | "userId", 10 | "username", 11 | "email", 12 | "password", 13 | ] 14 | 15 | def create(self, validated_data): 16 | user = CustomUserModel.objects.create_user( 17 | validated_data["username"], 18 | validated_data["email"], 19 | validated_data["password"] 20 | ) 21 | 22 | return user -------------------------------------------------------------------------------- /backend/nextjsdrfauth/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/nextjsdrfauth/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from .views import GoogleLoginView 3 | 4 | urlpatterns = [ 5 | path("google/", GoogleLoginView.as_view(), name = "google"), 6 | ] -------------------------------------------------------------------------------- /backend/nextjsdrfauth/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter 3 | from dj_rest_auth.registration.views import SocialLoginView 4 | from allauth.socialaccount.providers.oauth2.client import OAuth2Client 5 | from django.conf import settings 6 | 7 | class GoogleLoginView(SocialLoginView): 8 | authentication_classes = [] # disable authentication, make sure to override `allowed origins` in settings.py in production! 9 | adapter_class = GoogleOAuth2Adapter 10 | callback_url = "http://localhost:3000" # frontend application url 11 | client_class = OAuth2Client 12 | -------------------------------------------------------------------------------- /backend/nextjsdrfbackend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahieyin-rahmun/NextJsWithDRFExample/6a4140446ca05b4858c7fd74ca537efac5442178/backend/nextjsdrfbackend/__init__.py -------------------------------------------------------------------------------- /backend/nextjsdrfbackend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for nextjsdrfbackend 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.1/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', 'nextjsdrfbackend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/nextjsdrfbackend/secrets.example.py: -------------------------------------------------------------------------------- 1 | DJANGO_SECRET_KEY = "your django secret key" 2 | JWT_SECRET_KEY = "your jwt signing secret key" -------------------------------------------------------------------------------- /backend/nextjsdrfbackend/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from .secrets import DJANGO_SECRET_KEY, JWT_SECRET_KEY 3 | 4 | BASE_DIR = Path(__file__).resolve().parent.parent 5 | 6 | # Quick-start development settings - unsuitable for production 7 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = DJANGO_SECRET_KEY 11 | 12 | # SECURITY WARNING: don't run with debug turned on in production! 13 | DEBUG = True 14 | 15 | ALLOWED_HOSTS = [] 16 | 17 | # Application definition 18 | 19 | INSTALLED_APPS = [ 20 | 'django.contrib.admin', 21 | 'django.contrib.auth', 22 | 'django.contrib.contenttypes', 23 | 'django.contrib.sessions', 24 | 'django.contrib.messages', 25 | 'django.contrib.staticfiles', 26 | 27 | # third party 28 | "rest_framework", 29 | 'rest_framework.authtoken', 30 | "corsheaders", 31 | 'django.contrib.sites', 32 | 33 | # authentication 34 | 'dj_rest_auth', 35 | 'dj_rest_auth.registration', 36 | 'allauth', 37 | 'allauth.account', 38 | 'allauth.socialaccount', 39 | 'allauth.socialaccount.providers.google', 40 | 41 | # local 42 | "nextjsdrfauth" 43 | ] 44 | 45 | SOCIALACCOUNT_PROVIDERS = { 46 | 'google': { 47 | 'SCOPE': [ 48 | 'profile', 49 | 'email', 50 | ], 51 | 'AUTH_PARAMS': { 52 | 'access_type': 'online', 53 | } 54 | } 55 | } 56 | 57 | # we are turning off email verification for now 58 | SOCIALACCOUNT_EMAIL_VERIFICATION = "none" 59 | SOCIALACCOUNT_EMAIL_REQUIRED = False 60 | 61 | SITE_ID = 1 # https://dj-rest-auth.readthedocs.io/en/latest/installation.html#registration-optional 62 | REST_USE_JWT = True # use JSON Web Tokens 63 | # JWT_AUTH_COOKIE = "nextjsdrf-access-token" 64 | # JWT_AUTH_REFRESH_COOKIE = "nextjsdrf-refresh-token" 65 | # JWT_AUTH_SAMESITE = "none" 66 | 67 | 68 | MIDDLEWARE = [ 69 | "corsheaders.middleware.CorsMiddleware", 70 | 'django.middleware.security.SecurityMiddleware', 71 | 'django.contrib.sessions.middleware.SessionMiddleware', 72 | 'django.middleware.common.CommonMiddleware', 73 | 'django.middleware.csrf.CsrfViewMiddleware', 74 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 75 | 'django.contrib.messages.middleware.MessageMiddleware', 76 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 77 | ] 78 | 79 | from datetime import timedelta 80 | 81 | SIMPLE_JWT = { 82 | 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), 83 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 84 | 'ROTATE_REFRESH_TOKENS': True, 85 | 'BLACKLIST_AFTER_ROTATION': True, 86 | 'UPDATE_LAST_LOGIN': True, 87 | "USER_ID_FIELD": "userId", # for the custom user model 88 | "USER_ID_CLAIM": "user_id", 89 | "SIGNING_KEY": JWT_SECRET_KEY 90 | } 91 | 92 | CORS_ORIGIN_ALLOW_ALL = True # only for dev environment!, this should be changed before you push to production 93 | 94 | # custom user model, because we do not want to use the Django provided user model 95 | AUTH_USER_MODEL = "nextjsdrfauth.CustomUserModel" 96 | # We need to specify the exact serializer as well for dj-rest-auth, otherwise it will end up shooting itself 97 | # in the foot and me in the head 98 | REST_AUTH_SERIALIZERS = { 99 | 'USER_DETAILS_SERIALIZER': 'nextjsdrfauth.serializers.CustomUserModelSerializer' 100 | } 101 | 102 | # set up the authentication classes 103 | REST_FRAMEWORK = { 104 | "DEFAULT_PERMISSION_CLASSES": ( 105 | "rest_framework.permissions.IsAuthenticated", 106 | ), 107 | 108 | "DEFAULT_AUTHENTICATION_CLASSES": ( 109 | "rest_framework.authentication.BasicAuthentication", 110 | "rest_framework.authentication.SessionAuthentication", 111 | "dj_rest_auth.utils.JWTCookieAuthentication", 112 | ), 113 | } 114 | 115 | ROOT_URLCONF = 'nextjsdrfbackend.urls' 116 | 117 | TEMPLATES = [ 118 | { 119 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 120 | 'DIRS': [], 121 | 'APP_DIRS': True, 122 | 'OPTIONS': { 123 | 'context_processors': [ 124 | 'django.template.context_processors.debug', 125 | 'django.template.context_processors.request', 126 | 'django.contrib.auth.context_processors.auth', 127 | 'django.contrib.messages.context_processors.messages', 128 | ], 129 | }, 130 | }, 131 | ] 132 | 133 | WSGI_APPLICATION = 'nextjsdrfbackend.wsgi.application' 134 | 135 | 136 | # Database 137 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 138 | 139 | DATABASES = { 140 | 'default': { 141 | 'ENGINE': 'django.db.backends.sqlite3', 142 | 'NAME': BASE_DIR / 'db.sqlite3', 143 | } 144 | } 145 | 146 | 147 | # Password validation 148 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 149 | 150 | AUTH_PASSWORD_VALIDATORS = [ 151 | { 152 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 153 | }, 154 | { 155 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 156 | }, 157 | { 158 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 159 | }, 160 | { 161 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 162 | }, 163 | ] 164 | 165 | 166 | # Internationalization 167 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 168 | 169 | LANGUAGE_CODE = 'en-us' 170 | 171 | TIME_ZONE = 'UTC' 172 | 173 | USE_I18N = True 174 | 175 | USE_L10N = True 176 | 177 | USE_TZ = True 178 | 179 | 180 | # Static files (CSS, JavaScript, Images) 181 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 182 | 183 | STATIC_URL = '/static/' 184 | -------------------------------------------------------------------------------- /backend/nextjsdrfbackend/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/posts/", include("posts.urls")), 7 | path("api/auth/", include("dj_rest_auth.urls")), # endpoints provided by dj-rest-auth 8 | path("api/social/login/", include("nextjsdrfauth.urls")), # our own views 9 | ] 10 | -------------------------------------------------------------------------------- /backend/nextjsdrfbackend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for nextjsdrfbackend 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.1/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', 'nextjsdrfbackend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahieyin-rahmun/NextJsWithDRFExample/6a4140446ca05b4858c7fd74ca537efac5442178/backend/posts/__init__.py -------------------------------------------------------------------------------- /backend/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /backend/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | # Create your models here. 3 | 4 | -------------------------------------------------------------------------------- /backend/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | PostListView 4 | ) 5 | 6 | urlpatterns = [ 7 | path("", PostListView.as_view()), 8 | ] 9 | -------------------------------------------------------------------------------- /backend/posts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | import json 3 | from django.http import JsonResponse 4 | from rest_framework.views import APIView 5 | import requests 6 | # Create your views here. 7 | 8 | class PostListView(APIView): 9 | def get(self, request, *args, **kwargs): 10 | posts = requests.get("https://jsonplaceholder.typicode.com/posts") 11 | content = posts.content 12 | stringified = content.decode('utf8').replace("'", '"') 13 | stringified = json.loads(stringified) 14 | return JsonResponse(stringified, safe=False) 15 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.3.1 2 | certifi==2020.12.5 3 | cffi==1.14.5 4 | chardet==4.0.0 5 | cryptography==3.4.5 6 | defusedxml==0.6.0 7 | dj-rest-auth==2.1.3 8 | Django 9 | django-allauth==0.44.0 10 | django-cors-headers==3.7.0 11 | djangorestframework==3.12.2 12 | djangorestframework-simplejwt==4.6.0 13 | idna==2.10 14 | mccabe==0.6.1 15 | oauthlib==3.1.0 16 | pycparser==2.20 17 | PyJWT==2.0.1 18 | python3-openid==3.2.0 19 | pytz==2021.1 20 | requests==2.25.1 21 | requests-oauthlib==1.3.0 22 | sqlparse==0.4.1 23 | urllib3 24 | wrapt==1.11.2 25 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_URL= 2 | 3 | GOOGLE_CLIENT_ID= 4 | GOOGLE_CLIENT_SECRET= -------------------------------------------------------------------------------- /client/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 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | 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. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /client/components/Post.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TPost } from "../constants/Types"; 3 | 4 | type TPostProps = { 5 | post: TPost; 6 | }; 7 | 8 | function Post(props: TPostProps) { 9 | const { 10 | post: { userId, postId, body, title }, 11 | } = props; 12 | 13 | return ( 14 |
15 |

Post Title: {title}

16 | by User {userId} 17 |
{body}
18 |
19 |
20 | ); 21 | } 22 | 23 | export default Post; 24 | -------------------------------------------------------------------------------- /client/constants/HOCs.tsx: -------------------------------------------------------------------------------- 1 | import { Session } from "next-auth"; 2 | import { signIn } from "next-auth/client"; 3 | import React from "react"; 4 | import { useAuth } from "./Hooks"; 5 | 6 | type TSessionProps = { 7 | session: Session; 8 | }; 9 | 10 | export function withAuth

(refreshInterval?: number) { 11 | /* 12 | @param { number } refreshInterval: number of seconds before each refresh 13 | */ 14 | return function (Component: React.ComponentType

) { 15 | return function (props: Exclude) { 16 | const { session, loading } = useAuth(refreshInterval); 17 | 18 | if (typeof window !== undefined && loading) { 19 | return null; 20 | } 21 | 22 | if (!loading && !session) { 23 | return ( 24 | <> 25 | Not signed in
26 | 27 |

{"User is not logged in"}
28 | 29 | ); 30 | } 31 | 32 | return ; 33 | }; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /client/constants/Hooks.tsx: -------------------------------------------------------------------------------- 1 | import { Session } from "next-auth"; 2 | import { useEffect } from "react"; 3 | import useSwr, { mutate } from "swr"; 4 | 5 | // ### Failed approach using useState() ### 6 | // export function useAuth(refreshInterval?: number): [Session, boolean] { 7 | // /* 8 | // custom hook that keeps the session up-to-date by refreshing it 9 | 10 | // @param {number} refreshInterval: The refresh/polling interval in seconds. default is 20. 11 | // @return {tuple} A tuple of the Session and boolean 12 | // */ 13 | // const [session, setSession] = useState(null); 14 | // const [loading, setLoading] = useState(false); 15 | 16 | // useEffect(() => { 17 | // async function fetchSession() { 18 | // let sessionData: Session = null; 19 | // setLoading(true); 20 | 21 | // const session = await getSession({}); 22 | 23 | // if (session && Object.keys(session).length > 0) { 24 | // sessionData = session; 25 | // } 26 | 27 | // setSession((_) => sessionData); 28 | // setLoading(false); 29 | // } 30 | 31 | // refreshInterval = refreshInterval || 20; 32 | 33 | // fetchSession(); 34 | // const interval = setInterval(() => fetchSession(), refreshInterval * 1000); 35 | 36 | // return () => clearInterval(interval); 37 | // }, []); 38 | 39 | // return [session, loading]; 40 | // } 41 | 42 | const sessionUrl = "/api/auth/session"; 43 | 44 | async function fetchSession(url: string) { 45 | const response = await fetch(url); 46 | 47 | if (!response.ok) { 48 | throw new Error(`Could not fetch session from ${url}`); 49 | } 50 | 51 | const session: Session = await response.json(); 52 | 53 | if (!session || Object.keys(session).length === 0) { 54 | return null; 55 | } 56 | 57 | return session; 58 | } 59 | 60 | // ### useSwr() approach works for now ### 61 | export function useAuth(refreshInterval?: number) { 62 | /* 63 | custom hook that keeps the session up-to-date by refreshing it 64 | 65 | @param {number} refreshInterval: The refresh/polling interval in seconds. default is 20. 66 | @return {object} An object of the Session and boolean loading value 67 | */ 68 | const { data, error } = useSwr(sessionUrl, fetchSession, { 69 | revalidateOnFocus: true, 70 | revalidateOnMount: true, 71 | revalidateOnReconnect: true, 72 | }); 73 | 74 | useEffect(() => { 75 | const intervalId = setInterval( 76 | () => mutate(sessionUrl), 77 | (refreshInterval || 20) * 1000, 78 | ); 79 | 80 | return () => clearInterval(intervalId); 81 | }, []); 82 | 83 | return { 84 | session: data, 85 | loading: typeof data === "undefined" && typeof error === "undefined", 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /client/constants/Types.ts: -------------------------------------------------------------------------------- 1 | export type TPost = { 2 | userId: number; 3 | postId: number; 4 | title: string; 5 | body: string; 6 | }; 7 | -------------------------------------------------------------------------------- /client/constants/Utils.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | export namespace JwtUtils { 4 | export const isJwtExpired = (token: string) => { 5 | // offset by 60 seconds, so we will check if the token is "almost expired". 6 | const currentTime = Math.round(Date.now() / 1000 + 60); 7 | const decoded = jwt.decode(token); 8 | 9 | console.log(`Current time + 60 seconds: ${new Date(currentTime * 1000)}`); 10 | console.log(`Token lifetime: ${new Date(decoded["exp"] * 1000)}`); 11 | 12 | if (decoded["exp"]) { 13 | const adjustedExpiry = decoded["exp"]; 14 | 15 | if (adjustedExpiry < currentTime) { 16 | console.log("Token expired"); 17 | return true; 18 | } 19 | 20 | console.log("Token has not expired yet"); 21 | return false; 22 | } 23 | 24 | console.log('Token["exp"] does not exist'); 25 | return true; 26 | }; 27 | } 28 | 29 | export namespace UrlUtils { 30 | export const makeUrl = (...endpoints: string[]) => { 31 | let url = endpoints.reduce((prevUrl, currentPath) => { 32 | if (prevUrl.length === 0) { 33 | return prevUrl + currentPath; 34 | } 35 | 36 | return prevUrl.endsWith("/") 37 | ? prevUrl + currentPath + "/" 38 | : prevUrl + "/" + currentPath + "/"; 39 | }, ""); 40 | return url; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "cookie": "^0.4.1", 13 | "jsonwebtoken": "^8.5.1", 14 | "lodash": "^4.17.21", 15 | "next": "10.2.0", 16 | "next-auth": "^3.20.1", 17 | "react": "17.0.2", 18 | "react-dom": "17.0.2", 19 | "swr": "^0.5.6" 20 | }, 21 | "devDependencies": { 22 | "@types/cookie": "^0.4.0", 23 | "@types/jsonwebtoken": "^8.5.1", 24 | "@types/lodash": "^4.14.168", 25 | "@types/next-auth": "^3.15.0", 26 | "@types/node": "^15.0.2", 27 | "@types/react": "^17.0.5", 28 | "typescript": "^4.2.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from "next/app"; 2 | 3 | function MyApp({ Component, pageProps }: AppProps) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /client/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import NextAuth from "next-auth"; 3 | import { NextAuthOptions } from "next-auth"; 4 | import Providers from "next-auth/providers"; 5 | import axios from "axios"; 6 | import { JwtUtils, UrlUtils } from "../../../constants/Utils"; 7 | 8 | namespace NextAuthUtils { 9 | export const refreshToken = async function (refreshToken) { 10 | try { 11 | const response = await axios.post( 12 | // "http://localhost:8000/api/auth/token/refresh/", 13 | UrlUtils.makeUrl( 14 | process.env.BACKEND_API_BASE, 15 | "auth", 16 | "token", 17 | "refresh", 18 | ), 19 | { 20 | refresh: refreshToken, 21 | }, 22 | ); 23 | 24 | const { access, refresh } = response.data; 25 | // still within this block, return true 26 | return [access, refresh]; 27 | } catch { 28 | return [null, null]; 29 | } 30 | }; 31 | } 32 | 33 | const settings: NextAuthOptions = { 34 | secret: process.env.SESSION_SECRET, 35 | session: { 36 | jwt: true, 37 | maxAge: 24 * 60 * 60, // 24 hours 38 | }, 39 | jwt: { 40 | secret: process.env.JWT_SECRET, 41 | }, 42 | debug: process.env.NODE_ENV === "development", 43 | providers: [ 44 | Providers.Google({ 45 | clientId: process.env.GOOGLE_CLIENT_ID, 46 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 47 | }), 48 | ], 49 | callbacks: { 50 | async jwt(token, user, account, profile, isNewUser) { 51 | // user just signed in 52 | if (user) { 53 | // may have to switch it up a bit for other providers 54 | if (account.provider === "google") { 55 | // extract these two tokens 56 | const { accessToken, idToken } = account; 57 | 58 | // make a POST request to the DRF backend 59 | try { 60 | const response = await axios.post( 61 | // tip: use a seperate .ts file or json file to store such URL endpoints 62 | // "http://127.0.0.1:8000/api/social/login/google/", 63 | UrlUtils.makeUrl( 64 | process.env.BACKEND_API_BASE, 65 | "social", 66 | "login", 67 | account.provider, 68 | ), 69 | { 70 | access_token: accessToken, // note the differences in key and value variable names 71 | id_token: idToken, 72 | }, 73 | ); 74 | 75 | // extract the returned token from the DRF backend and add it to the `user` object 76 | const { access_token, refresh_token } = response.data; 77 | // reform the `token` object from the access token we appended to the `user` object 78 | token = { 79 | ...token, 80 | accessToken: access_token, 81 | refreshToken: refresh_token, 82 | }; 83 | 84 | return token; 85 | } catch (error) { 86 | return null; 87 | } 88 | } 89 | } 90 | 91 | // user was signed in previously, we want to check if the token needs refreshing 92 | // token has been invalidated, try refreshing it 93 | if (JwtUtils.isJwtExpired(token.accessToken as string)) { 94 | const [ 95 | newAccessToken, 96 | newRefreshToken, 97 | ] = await NextAuthUtils.refreshToken(token.refreshToken); 98 | 99 | if (newAccessToken && newRefreshToken) { 100 | token = { 101 | ...token, 102 | accessToken: newAccessToken, 103 | refreshToken: newRefreshToken, 104 | iat: Math.floor(Date.now() / 1000), 105 | exp: Math.floor(Date.now() / 1000 + 2 * 60 * 60), 106 | }; 107 | 108 | return token; 109 | } 110 | 111 | // unable to refresh tokens from DRF backend, invalidate the token 112 | return { 113 | ...token, 114 | exp: 0, 115 | }; 116 | } 117 | 118 | // token valid 119 | return token; 120 | }, 121 | 122 | async session(session, userOrToken) { 123 | session.accessToken = userOrToken.accessToken; 124 | return session; 125 | }, 126 | }, 127 | }; 128 | 129 | export default (req: NextApiRequest, res: NextApiResponse) => 130 | NextAuth(req, res, settings); 131 | -------------------------------------------------------------------------------- /client/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { signOut } from "next-auth/client"; 2 | import Link from "next/link"; 3 | import { withAuth } from "../constants/HOCs"; 4 | 5 | function Home(props) { 6 | const { session } = props; 7 | return ( 8 | session && ( 9 | <> 10 | Signed in as {session.user.email}
11 | 12 | {session.accessToken &&
User has access token
} 13 | Go to posts 14 | 15 | ) 16 | ); 17 | } 18 | 19 | export default withAuth(3 * 60)(Home); 20 | -------------------------------------------------------------------------------- /client/pages/posts.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Post from "../components/Post"; 3 | import { withAuth } from "../constants/HOCs"; 4 | import { TPost } from "../constants/Types"; 5 | import Link from "next/link"; 6 | import { isEqual } from "lodash"; 7 | 8 | function Posts(props) { 9 | const [posts, setPosts] = useState([]); 10 | const [fetchingPosts, setFetchingPosts] = useState(false); 11 | const { session } = props; 12 | 13 | useEffect(() => { 14 | if (!session) { 15 | return; 16 | } 17 | 18 | async function getPosts() { 19 | setFetchingPosts(true); 20 | const response = await fetch("http://127.0.0.1:8000/api/posts", { 21 | method: "get", 22 | headers: new Headers({ 23 | Authorization: `Bearer ${session?.accessToken}`, 24 | }), 25 | }); 26 | 27 | if (response.ok) { 28 | const postData: TPost[] = await response.json(); 29 | 30 | if (!isEqual(posts, postData)) { 31 | setPosts(postData); 32 | } 33 | } 34 | 35 | setFetchingPosts(false); 36 | } 37 | 38 | // initiate the post fetching mechanism once 39 | getPosts(); 40 | const intervalId = setInterval(() => getPosts(), 10 * 1000); 41 | 42 | // useEffect cleanup 43 | return () => clearInterval(intervalId); 44 | }, [session]); 45 | 46 | return ( 47 |
48 |

Fetched at {JSON.stringify(new Date())}

49 | Back to homepage 50 | {posts.map((post) => ( 51 | 52 | ))} 53 |
54 | ); 55 | } 56 | 57 | export default withAuth(3 * 60)(Posts); 58 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahieyin-rahmun/NextJsWithDRFExample/6a4140446ca05b4858c7fd74ca537efac5442178/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "include": [ 22 | "next-env.d.ts", 23 | "**/*.ts", 24 | "**/*.tsx" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /client/types.ts: -------------------------------------------------------------------------------- 1 | import { User } from "next-auth"; 2 | 3 | export interface AuthenticatedUser extends User { 4 | accessToken?: string, 5 | refreshToken?: string, 6 | } --------------------------------------------------------------------------------