├── 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 |
2 |
3 |

4 |
To see this Demo in action please login.
5 |
6 |
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 |
2 |
3 |
{{ name }}
4 |
5 |
6 |
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 |
2 |
3 |
{{ name }}
4 |
Ping
5 |
{{this.pingValue}}
6 |
7 |
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 |
2 |
3 |
4 | Vue JWT demo
5 |
6 |
7 |
8 |
9 |
10 | Login
11 | Logout
12 | Ping
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
38 |
39 |
61 |
--------------------------------------------------------------------------------
/jwt-vue/src/components/LoginForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
28 |
29 |
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 |
--------------------------------------------------------------------------------