├── server ├── public │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── models.py │ ├── admin.py │ ├── tests.py │ ├── apps.py │ ├── urls.py │ └── views.py ├── server │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── requirements.txt ├── manage.py ├── README.md └── .gitignore ├── jwt-vue ├── .browserslistrc ├── jest.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ └── logo.png │ ├── __mocks__ │ │ └── axios.js │ ├── views │ │ ├── Home.vue │ │ ├── Login.vue │ │ └── AuthenticatedPing.vue │ ├── services │ │ └── api │ │ │ ├── ping.js │ │ │ └── auth.js │ ├── main.js │ ├── store │ │ └── index.js │ ├── router │ │ └── index.js │ ├── App.vue │ └── components │ │ └── LoginForm.vue ├── babel.config.js ├── .editorconfig ├── .gitignore ├── README.md ├── .eslintrc.js ├── package.json └── tests │ └── unit │ └── services │ └── api │ └── auth.spec.js ├── .github ├── dependabot.yml └── workflows │ ├── automerge.yml │ ├── django.yml │ └── node.js.yml ├── LICENSE └── README.md /server/public/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/public/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jwt-vue/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /server/public/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /jwt-vue/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest', 3 | }; 4 | -------------------------------------------------------------------------------- /server/public/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /server/public/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /jwt-vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimpleJWT/drf-SimpleJWT-Vue/HEAD/jwt-vue/public/favicon.ico -------------------------------------------------------------------------------- /jwt-vue/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimpleJWT/drf-SimpleJWT-Vue/HEAD/jwt-vue/src/assets/logo.png -------------------------------------------------------------------------------- /jwt-vue/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /server/public/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PublicConfig(AppConfig): 5 | name = 'public' 6 | -------------------------------------------------------------------------------- /jwt-vue/src/__mocks__/axios.js: -------------------------------------------------------------------------------- 1 | const mockAxios = jest.genMockFromModule('axios'); 2 | 3 | mockAxios.create.mockReturnThis(); 4 | 5 | export default mockAxios; 6 | -------------------------------------------------------------------------------- /jwt-vue/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.3.1 2 | Django==3.1.7 3 | django-cors-headers==3.7.0 4 | djangorestframework==3.12.2 5 | djangorestframework-simplejwt==4.6.0 6 | PyJWT==2.0.1 7 | pytz==2021.1 8 | sqlparse==0.4.1 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/jwt-vue" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: pip 9 | directory: "/server" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /jwt-vue/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /jwt-vue/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /jwt-vue/src/services/api/ping.js: -------------------------------------------------------------------------------- 1 | import { authRequest } from './auth'; 2 | 3 | const ping = () => { 4 | const extraParameters = { params: { id: 'PONG' } }; 5 | return authRequest.get('/api/ping/', extraParameters) 6 | .then((response) => Promise.resolve(response.data)) 7 | .catch((error) => Promise.reject(error)); 8 | }; 9 | 10 | export { ping }; // eslint-disable-line import/prefer-default-export 11 | -------------------------------------------------------------------------------- /server/public/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.3 on 2020-03-02 21:13 2 | 3 | from django.db import migrations 4 | from django.contrib.auth.models import User 5 | 6 | 7 | def create_user(apps, schema_editor): 8 | User.objects.create_superuser("test", password="test") 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.RunPython(create_user) 18 | ] 19 | -------------------------------------------------------------------------------- /server/server/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for server 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.0/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', 'server.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /server/server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for server 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.0/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', 'server.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /jwt-vue/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | -------------------------------------------------------------------------------- /jwt-vue/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'; 3 | import App from './App.vue'; 4 | import router from './router'; 5 | import store from './store'; 6 | 7 | import 'bootstrap/dist/css/bootstrap.css'; 8 | import 'bootstrap-vue/dist/bootstrap-vue.css'; 9 | 10 | Vue.use(BootstrapVue); 11 | Vue.use(IconsPlugin); 12 | 13 | Vue.config.productionTip = false; 14 | 15 | new Vue({ 16 | router, 17 | store, 18 | render: (h) => h(App), 19 | }).$mount('#app'); 20 | -------------------------------------------------------------------------------- /jwt-vue/README.md: -------------------------------------------------------------------------------- 1 | # test-project 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your unit tests 19 | ``` 20 | npm run test:unit 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /jwt-vue/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/airbnb', 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint', 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | }, 17 | overrides: [ 18 | { 19 | files: [ 20 | '**/__tests__/*.{j,t}s?(x)', 21 | '**/tests/unit/**/*.spec.{j,t}s?(x)', 22 | ], 23 | env: { 24 | jest: true, 25 | }, 26 | }, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /jwt-vue/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server/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 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /jwt-vue/src/views/AuthenticatedPing.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | -------------------------------------------------------------------------------- /server/public/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from . import views 3 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView 4 | 5 | from rest_framework import routers 6 | router = routers.DefaultRouter() 7 | router.register('ping', views.PingViewSet, basename="ping") 8 | 9 | urlpatterns = [ 10 | path('api/token/access/', TokenRefreshView.as_view(), name='token_get_access'), 11 | path('api/token/both/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 12 | path('api/', include(router.urls)) 13 | ] 14 | 15 | """ 16 | - For the first view, you send the refresh token to get a new access token. 17 | - For the second view, you send the client credentials (username and password) 18 | to get BOTH a new access and refresh token. 19 | """ 20 | -------------------------------------------------------------------------------- /server/public/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.viewsets import GenericViewSet 2 | from rest_framework.mixins import ListModelMixin 3 | from rest_framework.response import Response 4 | from rest_framework.status import HTTP_200_OK 5 | from rest_framework.permissions import IsAuthenticated 6 | 7 | 8 | class PingViewSet(GenericViewSet, ListModelMixin): 9 | """ 10 | Helpful class for internal health checks 11 | for when your server deploys. Typical of AWS 12 | applications behind ALB which does default 30 13 | second ping/health checks. 14 | """ 15 | permission_classes = [IsAuthenticated] 16 | 17 | def list(self, request, *args, **kwargs): 18 | return Response( 19 | data={"id": request.GET.get("id")}, 20 | status=HTTP_200_OK 21 | ) 22 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: "Dependabot Automerge - Action" 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | worker: 8 | runs-on: ubuntu-latest 9 | 10 | if: github.actor == 'dependabot[bot]' 11 | steps: 12 | - name: 'Wait for status checks' 13 | id: waitforstatuschecks 14 | uses: WyriHaximus/github-action-wait-for-status@v1.2.0 15 | with: 16 | ignoreActions: worker,WIP 17 | checkInterval: 300 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: 'Automerge' 22 | uses: pascalgn/automerge-action@v0.11.0 23 | if: steps.waitforstatuschecks.outputs.status == 'success' 24 | env: 25 | MERGE_LABELS: "dependencies" 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | MERGE_DELETE_BRANCH: true 28 | -------------------------------------------------------------------------------- /server/server/urls.py: -------------------------------------------------------------------------------- 1 | """server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('', include('public.urls')) 22 | ] 23 | -------------------------------------------------------------------------------- /jwt-vue/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import { loginUser, logoutUser } from '../services/api/auth'; 4 | 5 | Vue.use(Vuex); 6 | 7 | export default new Vuex.Store({ 8 | state: { 9 | user: null, 10 | isLoggedIn: false, 11 | }, 12 | mutations: { 13 | loginSuccess(state, userId) { 14 | state.user = userId; 15 | state.isLoggedIn = true; 16 | }, 17 | logout(state) { 18 | state.user = null; 19 | state.isLoggedIn = false; 20 | }, 21 | }, 22 | actions: { 23 | login({ commit }, { username, password }) { 24 | return loginUser(username, password) 25 | .then(() => { 26 | commit({ type: 'loginSuccess', username }); 27 | return Promise.resolve(); 28 | }).catch((error) => { 29 | commit({ type: 'logout' }); 30 | return Promise.reject(error); 31 | }); 32 | }, 33 | logout({ commit }) { 34 | logoutUser(); 35 | commit('logout'); 36 | }, 37 | }, 38 | modules: { 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SimpleJWT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - 'server/**' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - 'server/**' 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | working-directory: ./server 20 | 21 | strategy: 22 | max-parallel: 4 23 | matrix: 24 | python-version: [3.7, 3.8, 3.9] 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - uses: actions/cache@v2 33 | with: 34 | path: ~/.cache/pip 35 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 36 | restore-keys: | 37 | ${{ runner.os }}-pip- 38 | - name: Install Dependencies 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install -r requirements.txt 42 | - name: Run Tests 43 | run: | 44 | python manage.py test 45 | -------------------------------------------------------------------------------- /jwt-vue/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import Home from '../views/Home.vue'; 4 | import Login from '../views/Login.vue'; 5 | import AuthenticatedPing from '../views/AuthenticatedPing.vue'; 6 | import { ACCESS_TOKEN, REFRESH_TOKEN } from '../services/api/auth'; 7 | 8 | Vue.use(VueRouter); 9 | 10 | const PUBLIC_PATHS = ['/', '/login']; 11 | 12 | const routes = [ 13 | { 14 | path: '/', 15 | name: 'Home', 16 | component: Home, 17 | }, 18 | { 19 | path: '/login', 20 | name: 'Login', 21 | component: Login, 22 | }, 23 | { 24 | path: '/ping', 25 | name: 'AuthenticatedPing', 26 | component: AuthenticatedPing, 27 | }, 28 | ]; 29 | 30 | const router = new VueRouter({ 31 | mode: 'history', 32 | base: process.env.BASE_URL, 33 | routes, 34 | }); 35 | 36 | const unAuthenticatedAndPrivatePage = (path) => (!PUBLIC_PATHS.includes(path) 37 | && !(ACCESS_TOKEN in window.localStorage) 38 | && !(REFRESH_TOKEN in window.localStorage)); 39 | 40 | router.beforeEach((to, from, next) => { 41 | if (unAuthenticatedAndPrivatePage(to.path)) { 42 | next(`/login?next=${to.path}`); 43 | } else { 44 | next(); 45 | } 46 | }); 47 | 48 | export default router; 49 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | path: 10 | - 'jwt-vue/**' 11 | pull_request: 12 | branches: [ master ] 13 | path: 14 | - 'jwt-vue/**' 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | working-directory: ./jwt-vue 22 | 23 | strategy: 24 | matrix: 25 | node-version: [10.x, 12.x, 14.x] 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v1 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | - uses: actions/cache@v2 34 | with: 35 | path: ~/.npm 36 | key: ${{ runner.os }}-node-${{ hashFiles('**/jwt-vue/package-lock.json') }} 37 | restore-keys: | 38 | ${{ runner.os }}-node- 39 | - run: npm ci 40 | - run: npm run build --if-present 41 | - run: npm run test:unit 42 | -------------------------------------------------------------------------------- /jwt-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-project", 3 | "version": "0.0.1", 4 | "description": "Django, DRF, DRF SimpleJWT with Vue Frontend sample.", 5 | "author": "Daniel Mouris", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "serve": "vue-cli-service serve", 10 | "build": "vue-cli-service build", 11 | "test:unit": "vue-cli-service test:unit", 12 | "lint": "vue-cli-service lint" 13 | }, 14 | "dependencies": { 15 | "axios": "^0.21.1", 16 | "bootstrap": "^4.6.0", 17 | "bootstrap-vue": "^2.21.2", 18 | "core-js": "^3.9.1", 19 | "vue": "^2.6.12", 20 | "vue-router": "^3.5.1", 21 | "vuex": "^3.6.2" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli-plugin-babel": "~4.5.11", 25 | "@vue/cli-plugin-eslint": "~4.5.11", 26 | "@vue/cli-plugin-router": "~4.5.11", 27 | "@vue/cli-plugin-unit-jest": "~4.5.11", 28 | "@vue/cli-plugin-vuex": "~4.5.11", 29 | "@vue/cli-service": "~4.5.11", 30 | "@vue/eslint-config-airbnb": "^5.3.0", 31 | "@vue/test-utils": "^1.1.3", 32 | "babel-eslint": "^10.1.0", 33 | "eslint": "^6.7.2", 34 | "eslint-plugin-import": "^2.20.2", 35 | "eslint-plugin-vue": "^7.7.0", 36 | "vue-template-compiler": "^2.6.11" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Django Server 2 | 3 | The backend works by using Django, Django Rest Framework, and DRF SimpleJWT. 4 | 5 | For this demonstration, SimpleJWT utilizes the refresh and access token methodology. The client sends its credentials to the server once and receives an access and refresh token. Everytime you want to do authentication on a view, the client will send the access token; however, that access token expires (in our case, in 5 minutes for security reasons). Once it expires, instead of resending the credentials, we use the refresh token to get a new access token. 6 | 7 | If the refresh token expires (after 1 day for security reasons), the client needs to send the username and password again. 8 | 9 | ### Running the server 10 | 11 | 1. Create a virtual environment and install the packages: `virtualenv venv && source venv/bin/activate && pip install -r requirements.txt`. 12 | - Again, make sure when you do this, you are inside the server directory on your terminal/cmd. 13 | - On Windows, you should do `venv\Scripts\activate` instead of `source venv/bin/activate` 14 | 2. Run the server: `python manage.py migrate && python manage.py runserver` 15 | 16 | A default user with the username `test` and password `test` have been created. 17 | 18 | ### Other suggestions 19 | 20 | I also suggest you use a rate limiter, either provided by Django Rest Framework or a more sophisticated one like django-ratelimit so that you can rate limit across your entire application, not just your REST API. -------------------------------------------------------------------------------- /jwt-vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 38 | 39 | 61 | -------------------------------------------------------------------------------- /jwt-vue/src/components/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 66 | 67 | 68 | 74 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Template Repository for DRF SimpleJWT + Vue.js Apps 2 | 3 | Initially created: 3 July 2020 4 | 5 | TL;DR: Django server repository setup for SimpleJWT. Test user: `test` and pw `test`. 6 | 7 | --- 8 | ### Example repositories 9 | 10 | - React: [SimpleJWT/drf-SimpleJWT-React](https://github.com/SimpleJWT/drf-SimpleJWT-React) 11 | - Vue: [SimpleJWT/drf-SimpleJWT-Vue](https://github.com/SimpleJWT/drf-SimpleJWT-Vue) 12 | - Android: [Andrew-Chen-Wang/mobile-auth-example](https://github.com/Andrew-Chen-Wang/mobile-auth-example) 13 | - iOS: [Andrew-Chen-Wang/mobile-auth-example](https://github.com/Andrew-Chen-Wang/mobile-auth-example) 14 | 15 | --- 16 | ### Introduction 17 | 18 | This template repository is dedicated to generating 19 | a Django + DRF server with SimpleJWT already setup. 20 | The purpose of this is to easily create repositories 21 | that demonstrate clear usage of SimpleJWT. 22 | 23 | If you're not using a frontend framework like React 24 | or some kind of mobile device not using a web browser, 25 | then please use session authentication. I.e. if you're 26 | using plain HTML with Jinja 2 template tags, use the 27 | built-in session authentication middlewear as that 28 | is proven to be the safest and thus far never broken 29 | method of secure authentication. 30 | 31 | Note: this template repository is adopted from 32 | [Andrew-Chen-Wang/mobile-auth-example](https://github.com/Andrew-Chen-Wang/mobile-auth-example) 33 | for Android and iOS usage. The license is Apache 2.0 34 | for that example repository. 35 | 36 | --- 37 | ### Usage 38 | 39 | #### Backend (Django) Instructions 40 | 41 | 1. `cd server` to get your terminal/cmd into the server directory. 42 | 2. To run the server, create a virtual environment `virtualenv venv && source venv/bin/activate`, install packages `pip install -r requirements.txt` -- the requirements.txt file is inside the server subdirectory -- and do `python manage.py migrate && python manage.py runserver`. 43 | - Again, make sure when you do this, you are inside the server directory on your terminal/cmd. 44 | - On Windows, you should do `venv\Scripts\activate` instead of `source venv/bin/activate` 45 | 3. If you're writing for an example repository, please create 46 | a new directory labeled with the name of the framework (e.g. jwt-ios), 47 | and add its `.gitignore`. Please use the 48 | [github/gitignore](https://github.com/github/gitignore) repository. 49 | Provide detailed instructions if necessary. 50 | 51 | A default user with the username `test` and password `test` have been created. 52 | 53 | This repository does not come with throttling, but **it is 54 | highly recommended that you add throttling to your entire 55 | project.** You can use a third-party package called 56 | Django-ratelimit or DRF's internal throttling mechanism. 57 | Django-ratelimit is more extensive -- covering Django views, 58 | as well -- and thus more supported by SimpleJWT. 59 | 60 | #### Frontend (jwt-vue) Instructions 61 | 62 | 1. `cd jwt-vue` to get your terminal/server into the frontend (vue) folder. 63 | 64 | 2. `npm install` to install all of the dependencies for the front end application. 65 | 66 | 3. `npm run serve` and you should be good to go, ensure that your backend is running on port `http://localhost:8000`, if you run it on another port/ip please change the `BASE_URL` in `jwt-vue/_seriv/api/auth.js` 67 | 68 | --- 69 | ### License 70 | 71 | This repository is licensed under the 72 | [MIT License](https://github.com/SimpleJWT/drf-SimpleJWT-server-template/blob/master/LICENSE). 73 | -------------------------------------------------------------------------------- /jwt-vue/src/services/api/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // this base url will be change based on 4 | // if you need to point to production. 5 | const BASE_URL = 'http://localhost:8000'; 6 | const ACCESS_TOKEN = 'access_token'; 7 | const REFRESH_TOKEN = 'refresh_token'; 8 | 9 | const tokenRequest = axios.create({ 10 | baseURL: BASE_URL, 11 | timeout: 5000, 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | accept: 'application/json', 15 | }, 16 | }); 17 | 18 | const loginUser = (username, password) => { 19 | const loginBody = { username, password }; 20 | return tokenRequest.post('/api/token/both/', loginBody) 21 | .then((response) => { 22 | window.localStorage.setItem(ACCESS_TOKEN, response.data.access); 23 | window.localStorage.setItem(REFRESH_TOKEN, response.data.refresh); 24 | return Promise.resolve(response.data); 25 | }).catch((error) => { 26 | console.log(error); 27 | return Promise.reject(error); 28 | }); 29 | }; 30 | 31 | const refreshToken = () => { 32 | const refreshBody = { refresh: window.localStorage.getItem(REFRESH_TOKEN) }; 33 | return tokenRequest.post('/api/token/access/', refreshBody) 34 | .then((response) => { 35 | window.localStorage.setItem(ACCESS_TOKEN, response.data.access); 36 | return Promise.resolve(response.data); 37 | }).catch((error) => Promise.reject(error)); 38 | }; 39 | 40 | const isCorrectRefreshError = (status) => status === 401; 41 | 42 | /* 43 | * authRequest 44 | * 45 | * This refreshes the request and retries the token if it is invalid. 46 | * This is what you use to create any requests that need the Tokens. 47 | * Reference: https://hackernoon.com/110percent-complete-jwt-authentication-with-django-and-react-2020-iejq34ta 48 | * 49 | * Example: 50 | * authRequest.get('/path/to/endpoint/',extraParameters) 51 | * .then(response=>{ 52 | * // do something with successful request 53 | * }).catch((error)=> { 54 | * // handle any errors. 55 | * }); 56 | */ 57 | const authRequest = axios.create({ 58 | baseURL: BASE_URL, 59 | timeout: 5000, 60 | headers: { 61 | Authorization: `Bearer ${window.localStorage.getItem(ACCESS_TOKEN)}`, 62 | 'Content-Type': 'application/json', 63 | }, 64 | }); 65 | 66 | const logoutUser = () => { 67 | window.localStorage.removeItem(ACCESS_TOKEN); 68 | window.localStorage.removeItem(REFRESH_TOKEN); 69 | authRequest.defaults.headers.Authorization = ''; 70 | }; 71 | 72 | const errorInterceptor = (error) => { 73 | const originalRequest = error.config; 74 | const { status } = error.response; 75 | if (isCorrectRefreshError(status)) { 76 | return refreshToken().then(() => { 77 | const headerAuthorization = `Bearer ${window.localStorage.getItem(ACCESS_TOKEN)}`; 78 | authRequest.defaults.headers.Authorization = headerAuthorization; 79 | originalRequest.headers.Authorization = headerAuthorization; 80 | return authRequest(originalRequest); 81 | }).catch((tokenRefreshError) => { 82 | // if token refresh fails, logout the user to avoid potential security risks. 83 | logoutUser(); 84 | return Promise.reject(tokenRefreshError); 85 | }); 86 | } 87 | return Promise.reject(error); 88 | }; 89 | 90 | authRequest.interceptors.response.use( 91 | (response) => response, // this is for all successful requests. 92 | (error) => errorInterceptor(error), // handle the request 93 | ); 94 | 95 | export { 96 | tokenRequest, loginUser, logoutUser, refreshToken, authRequest, 97 | errorInterceptor, BASE_URL, ACCESS_TOKEN, REFRESH_TOKEN, 98 | }; 99 | -------------------------------------------------------------------------------- /jwt-vue/tests/unit/services/api/auth.spec.js: -------------------------------------------------------------------------------- 1 | import mockAxios from 'axios'; // refer to __mocks__/axios.js 2 | 3 | import { tokenRequest, loginUser, logoutUser, refreshToken, authRequest, 4 | errorInterceptor, BASE_URL, ACCESS_TOKEN, REFRESH_TOKEN } from '@/services/api/auth.js' 5 | 6 | window.localStorage = localStorage; 7 | 8 | beforeEach(() => { 9 | jest.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem'); 10 | jest.spyOn(Object.getPrototypeOf(window.localStorage), 'getItem'); 11 | jest.spyOn(Object.getPrototypeOf(window.localStorage), 'removeItem'); 12 | }); 13 | 14 | const ACCESS_TOKEN_VALUE = "a token"; 15 | const REFRESH_TOKEN_VALUE = "another token"; 16 | 17 | describe("loginUser", ()=> { 18 | const EXPECTED_DATA = {data: {"access": ACCESS_TOKEN_VALUE, "refresh": REFRESH_TOKEN_VALUE}} 19 | 20 | test("login request is made, and saves token", () => { 21 | tokenRequest.post.mockResolvedValueOnce(EXPECTED_DATA) 22 | const USER = "username-test"; 23 | const PASSWORD = "password-test"; 24 | 25 | return loginUser(USER, PASSWORD).then((data)=> { 26 | expect(data).toEqual(EXPECTED_DATA["data"]) 27 | expect(mockAxios.post).toHaveBeenCalledWith('/api/token/both/', 28 | {"password": PASSWORD, "username": USER} 29 | ); 30 | expect(window.localStorage.setItem).toHaveBeenCalledWith(ACCESS_TOKEN, ACCESS_TOKEN_VALUE) 31 | expect(window.localStorage.setItem).toHaveBeenCalledWith(REFRESH_TOKEN, REFRESH_TOKEN_VALUE) 32 | }); 33 | }); 34 | }); 35 | 36 | describe("refreshToken", () => { 37 | const EXPECTED_DATA = {"data":{"access":"another token"}} 38 | test("token request is made and saves token.", () => { 39 | //setup mocks 40 | tokenRequest.post.mockResolvedValueOnce(EXPECTED_DATA) 41 | window.localStorage.getItem.mockReturnValueOnce(REFRESH_TOKEN) 42 | 43 | return refreshToken().then((data)=> { 44 | expect(data).toEqual(EXPECTED_DATA['data']) 45 | expect(window.localStorage.getItem).toHaveBeenCalledWith(REFRESH_TOKEN) 46 | expect(tokenRequest.post).toHaveBeenCalledWith('/api/token/access/', 47 | {"refresh": REFRESH_TOKEN} 48 | ); 49 | expect(window.localStorage.setItem).toHaveBeenCalledWith(ACCESS_TOKEN, ACCESS_TOKEN_VALUE) 50 | }); 51 | }); 52 | }); 53 | 54 | 55 | describe("logoutUser", () => { 56 | test("removes Authorization and localStorage", () => { 57 | logoutUser(); 58 | expect(window.localStorage.removeItem).toHaveBeenCalledWith(REFRESH_TOKEN); 59 | expect(window.localStorage.removeItem).toHaveBeenCalledWith(ACCESS_TOKEN); 60 | expect(authRequest.defaults.headers['Authorization']).toEqual("") 61 | }); 62 | }); 63 | 64 | describe("errorInterceptor", () => { 65 | const ERROR_RESPONSE = { 66 | status: 401, 67 | statusText: "Unauthorized" 68 | } 69 | test("refreshes token", () => { 70 | let errorConfig = { 71 | url: "/api/ping/", 72 | method: "get", 73 | headers: { 74 | Accept: "application/json, text/plain, */*", 75 | Authorization: "" 76 | } 77 | } 78 | const EXPECTED_DATA = {"data":{"access":"another token"}}; 79 | tokenRequest.post.mockResolvedValueOnce(EXPECTED_DATA); 80 | authRequest.get.mockResolvedValueOnce(EXPECTED_DATA); 81 | window.localStorage.getItem 82 | .mockReturnValueOnce(REFRESH_TOKEN_VALUE) // for refreshToken 83 | .mockReturnValueOnce(ACCESS_TOKEN_VALUE) // for the error interceptor 84 | 85 | return errorInterceptor({config: errorConfig, response: ERROR_RESPONSE}) 86 | .finally(()=> { 87 | expect(tokenRequest.post).toHaveBeenCalledWith('/api/token/access/', 88 | {"refresh": REFRESH_TOKEN} 89 | ); 90 | expect(errorConfig.headers['Authorization']).toEqual(`Bearer ${ACCESS_TOKEN_VALUE}`) 91 | expect(authRequest).toHaveBeenCalledWith(errorConfig) 92 | }); 93 | }); 94 | 95 | test("if the error intercept refreshToken fails, logout user", () => { 96 | const correctError = new Error("Token Failed"); 97 | tokenRequest.post.mockRejectedValue(correctError); 98 | let errorConfig = { 99 | url: "/api/ping/", 100 | method: "get", 101 | headers: { 102 | Accept: "application/json, text/plain, */*", 103 | Authorization: "" 104 | } 105 | } 106 | return errorInterceptor({config: errorConfig, response: ERROR_RESPONSE}) 107 | .catch((error)=> { 108 | expect(error).toEqual(correctError); 109 | }) 110 | .finally(()=> { 111 | expect(tokenRequest.post).toHaveBeenCalledWith('/api/token/access/', 112 | {"refresh": REFRESH_TOKEN} 113 | ); 114 | expect(errorConfig.headers['Authorization']).toEqual(``) 115 | expect(window.localStorage.removeItem).toHaveBeenCalledWith(REFRESH_TOKEN); 116 | expect(window.localStorage.removeItem).toHaveBeenCalledWith(ACCESS_TOKEN); 117 | expect(authRequest.defaults.headers['Authorization']).toEqual("") 118 | }); 119 | }); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /server/server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for server project. 3 | Generated by 'django-admin startproject' using Django 3.0.3. 4 | """ 5 | import os 6 | # IMPORTANT STUFF BELOW!!!!! 7 | # IMPORTANT STUFF BELOW!!!!! 8 | # IMPORTANT STUFF BELOW!!!!! 9 | # IMPORTANT STUFF BELOW!!!!! 10 | # IMPORTANT STUFF BELOW!!!!! 11 | # IMPORTANT STUFF BELOW!!!!! 12 | # IMPORTANT STUFF BELOW!!!!! 13 | 14 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 15 | # SECURITY WARNING: keep the secret key used in production secret! 16 | SECRET_KEY = '3hls$)92@wy0z^lq67@w1a(qzx4*$)pj)_*1$m!h$k#dl(odq&' 17 | # SECURITY WARNING: don't run with debug turned on in production! 18 | DEBUG = True 19 | 20 | # The last one is my private IP address for Android development 21 | ALLOWED_HOSTS = ["127.0.0.1", "localhost", "192.168.0.12"] 22 | 23 | # IMPORTANT STUFF 24 | # IMPORTANT STUFF 25 | # IMPORTANT STUFF 26 | # IMPORTANT STUFF 27 | # IMPORTANT STUFF 28 | # IMPORTANT STUFF 29 | # IMPORTANT STUFF 30 | # IMPORTANT STUFF 31 | 32 | INSTALLED_APPS = [ 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'rest_framework', 40 | 'rest_framework_simplejwt.token_blacklist', 41 | 'public' 42 | ] 43 | 44 | REST_FRAMEWORK = { 45 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 46 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 47 | ) 48 | } 49 | 50 | from datetime import timedelta 51 | 52 | SIMPLE_JWT_SIGNING_KEY = "b=72^ado*%1(v3r7rga9ch)03xr=d*f)lroz94kosf!61((9=i" 53 | 54 | 55 | SIMPLE_JWT = { 56 | 'ACCESS_TOKEN_LIFETIME': timedelta(seconds=5), 57 | 'REFRESH_TOKEN_LIFETIME': timedelta(seconds=15), 58 | 'ROTATE_REFRESH_TOKENS': False, 59 | 'BLACKLIST_AFTER_ROTATION': True, 60 | 61 | 'ALGORITHM': 'HS256', 62 | 'SIGNING_KEY': SIMPLE_JWT_SIGNING_KEY, 63 | 'VERIFYING_KEY': None, 64 | 'AUDIENCE': None, 65 | 'ISSUER': None, 66 | 67 | 'AUTH_HEADER_TYPES': ('Bearer',), 68 | 'USER_ID_FIELD': 'id', 69 | 'USER_ID_CLAIM': 'user_id', 70 | 71 | 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 72 | 'TOKEN_TYPE_CLAIM': 'token_type', 73 | 74 | 'JTI_CLAIM': 'jti', 75 | 76 | 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', 77 | 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), 78 | 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), 79 | } 80 | 81 | 82 | 83 | 84 | # Not as important... 85 | 86 | MIDDLEWARE = [ 87 | 'django.middleware.security.SecurityMiddleware', 88 | 'django.contrib.sessions.middleware.SessionMiddleware', 89 | 'corsheaders.middleware.CorsMiddleware', 90 | 'django.middleware.common.CommonMiddleware', 91 | 'django.middleware.csrf.CsrfViewMiddleware', 92 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 93 | 'django.contrib.messages.middleware.MessageMiddleware', 94 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 95 | ] 96 | 97 | ROOT_URLCONF = 'server.urls' 98 | 99 | TEMPLATES = [ 100 | { 101 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 102 | 'DIRS': [], 103 | 'APP_DIRS': True, 104 | 'OPTIONS': { 105 | 'context_processors': [ 106 | 'django.template.context_processors.debug', 107 | 'django.template.context_processors.request', 108 | 'django.contrib.auth.context_processors.auth', 109 | 'django.contrib.messages.context_processors.messages', 110 | ], 111 | }, 112 | }, 113 | ] 114 | 115 | WSGI_APPLICATION = 'server.wsgi.application' 116 | 117 | 118 | # Database 119 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 120 | 121 | DATABASES = { 122 | 'default': { 123 | 'ENGINE': 'django.db.backends.sqlite3', 124 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 125 | } 126 | } 127 | 128 | 129 | # Password validation 130 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 131 | 132 | AUTH_PASSWORD_VALIDATORS = [ 133 | { 134 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 135 | }, 136 | { 137 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 138 | }, 139 | { 140 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 141 | }, 142 | { 143 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 144 | }, 145 | ] 146 | 147 | 148 | # Internationalization 149 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 150 | 151 | LANGUAGE_CODE = 'en-us' 152 | 153 | TIME_ZONE = 'UTC' 154 | 155 | USE_I18N = True 156 | 157 | USE_L10N = True 158 | 159 | USE_TZ = True 160 | 161 | 162 | # Static files (CSS, JavaScript, Images) 163 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 164 | 165 | STATIC_URL = '/static/' 166 | 167 | # Cors Settings 168 | CORS_ALLOW_CREDENTIALS = True 169 | 170 | # NOTE: 171 | # change 'https://example-prod-vue.com' to your frontend domain 172 | CORS_ORIGIN_WHITELIST = [ 173 | 'http://localhost:8080', 174 | 'http://127.0.0.1:8080', 175 | 'https://example-prod-vue.com' 176 | ] 177 | --------------------------------------------------------------------------------