├── .browserslistrc ├── .eslintrc.js ├── .flake8 ├── .gitignore ├── README.md ├── babel.config.js ├── backend ├── __init__.py ├── adapters.py ├── asgi.py ├── posts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── tests.py │ └── views.py ├── settings.py ├── urls.py ├── views.py └── wsgi.py ├── manage.py ├── package.json ├── poetry.lock ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── pyproject.toml ├── src ├── App.vue ├── assets │ ├── css │ │ └── main.css │ └── logo.png ├── components │ └── HelloWorld.vue ├── main.js ├── router │ └── index.js ├── store │ └── index.js └── views │ ├── About.vue │ ├── Home.vue │ ├── Login.vue │ ├── NotFound.vue │ ├── PostDetail.vue │ ├── PostUpdate.vue │ ├── Signup.vue │ └── SubmitPost.vue ├── tailwind.config.js ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"], 7 | parserOptions: { 8 | parser: "babel-eslint" 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503, E231, E701 3 | max-line-length = 80 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | *.sqlite3 24 | __pycache__ 25 | *.egg-info 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a VERY basic, rough and ready demo of integrating Django and Vue into a single page application. 2 | 3 | It is not meant for production, but just provides some starting points for building a real-world project. It's lacking a lot of things that would make it so such as Docker (or other deployment/containerization system), backend or frontend unit and integration tests, etc etc. While I have provided some ideas here on how to build a secure application, this has not been tested "in the wild" and any risk in using this code is your own. 4 | 5 | One impetus for doing this is the lack of good examples out there in building a secure appliction. Most of the time, examples and tutorials use DRF auth tokens (and sometimes JWT) and storing the token(s) in localStorage. Perhaps this is OK for a toy app, but doing so without care and caveats opens up your application to XSS attacks. JWT is probably best suited for mobile and server-to-server API calls, but not for web browsers. I admit this is a controversial opinion, but current OWASP guidelines [1] recommend against storing authentication data in localStorage, sessionStorage and client-side cookies. 6 | 7 | Short of using an external service (e.g. Auth0), what is best practice for securing a Django/DRF-backed SPA? I would argue the same best practices you would follow in building a traditional server-rendered Django project: HttpOnly cookies along with CSRF protection for "write" endpoints (along with other best practices such as always using HTTPS, using SameSite cookie settings etc etc). 8 | 9 | Using DRF this is relatively straightforward: use SessionAuthentication. There is a problem however if you want to login or signup: SessionAuthentication applies CSRF only to authenticated users. There are perhaps good reasons for this, but it means that just adding some "login" or "signup" endpoints to your API will not be sufficient to prevent a serious attack vector. 10 | 11 | Another point: I would like to use django_allauth for authentication as opposed to Django auth views. Allauth provides more options and out-of-the-box functionality I would like to leverage. 12 | 13 | The official DRF recommendations for SessionAuthentication however recommend using plain auth views (or by extension, allauth or other functionality based on Django's auth system). However, django_allauth supports AJAX views out of the box (albeit with plain form submission parameters instead of JSON data). The allauth AJAX views return JSON, including fields and errors (they also include "html" i.e. the rendered HTML template: you can ignore this, but for completion there is a custom adapter that skips this generation for efficiency). These are plain Django views i.e. requiring CSRF which is set up in axios. Once you are authenticated, all DRF POST/PUT/DELETE etc will use CSRF through SessionAuthentication. 14 | 15 | The basic architecture works like this: 16 | 17 | 1. We have a Django TemplateView as the "default page" for our application. Any other Django URLs (DRF API routes, allauth, admin) must go before this view. It has a wildcard to ensure that any other URLs are managed client-side with vue-router, so if for example I enter "http://mysite.com/login" as my URL it will automatically show the login page instead of a 404. 18 | 2. The default page is pretty much a standard view. It is wrapped in the ensure_csrf_cookie decorator to make sure that when the user navigates to the page, a CSRF cookie will always be added. 19 | 3. In addition to the CSRF cookie, we want to render a Django template. This template injects anything we want to include in our Vue SPA on page load. For example, we want to know if a user is logged in, and the details of that user needed for the application (i.e. request.user). The user is serialized and added into the page using the json_script template filter, which allows the Vue SPA to just load and parse that tag at startup. This is very useful as it simplifies and speeds up the loading process: we can hydrate our Vuex store synchronously on initial load rather than wait for one or more API calls to provide the initial data. 20 | 4. Note that the Django template (index.html) is not located under "templates" as in a common Django app. Vue cli instead looks in the "dist" directory when building the assets and adds all the required paths to the assets (e.g. JS and CSS bundles) to that template. This lets Vue/Webpack inject all the required assets and other info into the template, and Django inject the request-time info needed to hydrate the SPA. 21 | 5. So: vue-cli injects the correct paths into a Django template. You just navigate to your Django app (http://localhost:8000 or whatever the production URL is) and the template with all the ready links will just work. However there is a caveat: in development, the hot-loader service won't work. Instead we remove that and use the "watch" 22 | service to rebuild immediately when a file is changed. This isn't as slick as hot-reloading but not sure of a way around this trade-off without adding inconsistency and complexity to the workflow (i.e. things not quite working the same way in production, being able to hydrate and access the CSRF token etc). I'm happy with the tradeoff as I often find myself doing a full refresh on edit anyway, but others might find it annoying. 23 | 6. I've used whitenoise for ease of development and deployment. The vue config should probably be set up to inject the URL for your CDN (e.g. Cloudfront) as an environment variable in your pipeline, and you then configure your CDN to point the domain back to your app. Please consult the Vue and whitenoise docs for more details. 24 | 7. Because everything is just served under one domain, as one application, you don't need CORS. 25 | 26 | My aim is to be able to leverage all the advantages of Django and its ecosystem: the ORM, Rest Framework, tried-and-tested security - with the advantages of Vue and its ecosystem - components, fast turnaround, good performance and so on. I wasn't willing however to compromise on security for the sake of DX and the oft-quoted practice of "just use localStorage for your auth tokens" seemed a bit fishy in the face of expert recommendations. 27 | 28 | I did look at django-webpack-loader. I found it a bit awkward for a number of reasons: development seemed quite slow and wasn't up to date with the latest Webpack updates (this may have since changed); and the need to have the extra step of tracking all the paths in a manifest looked like it would require more Webpack hacking and patching than I was comfortable with, sacrificing much of the ease of use provided by vue-cli and causing headaches for a CI/CD pipeline. 29 | 30 | What about a multi-page setup? I've not tried it but this might work: https://cli.vuejs.org/config/#pages. So you map the pages to Django templates in the same way as the single index.html, and have different Django views serve up those pages. Alternatively, you could just use the generic single template and inject different JSON loads at startup (maybe namespacing to easily sync with Vuex). This might work better where you don't have an SPA but instead "mini-applications" along with static content. If you need actual static content for SEO purposes you can of course just serve up plain Django views or pregenerated HTML static pages. There are different use cases and no one size fits all. 31 | 32 | Deployment: the main issue is being able to generate and access the index.html file under dist. Unlike JS and other assets we need to keep this file accessible to the Django app locally. 33 | 34 | A pipeline that for example does a Heroku deploy would have to generate this file as part of the build process and maybe use COPY to ensure it's part of the deployment (e.g. gitlab artifacts) 35 | 36 | 37 | 38 | GETTING STARTED 39 | 40 | ` 41 | yarn 42 | ` 43 | 44 | ` 45 | poetry install 46 | ` 47 | 48 | ` 49 | poetry run ./manage.py migrate 50 | ` 51 | 52 | In separate terminal windows: 53 | 54 | ` 55 | yarn dev 56 | ` 57 | 58 | ` 59 | poetry run ./manage.py runserver 60 | ` 61 | 62 | CREDITS 63 | 64 | This repo provided a lot of useful pointers, particularly in setting up Django template with Vue: 65 | 66 | https://github.com/gtalarico/django-vue-template 67 | 68 | 69 | LINKS 70 | 71 | [1] https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTML5_Security_Cheat_Sheet.md#local-storage 72 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danjac/django-vue-auth-demo/14440162986e0c2451303e25d52d6fca55d180d0/backend/__init__.py -------------------------------------------------------------------------------- /backend/adapters.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | 3 | from allauth.account.adapter import DefaultAccountAdapter 4 | 5 | 6 | class CustomAccountAdapter(DefaultAccountAdapter): 7 | def ajax_response(self, request, response, redirect_to=None, form=None, data=None): 8 | resp = {} 9 | status = response.status_code 10 | 11 | if redirect_to: 12 | status = 200 13 | resp["location"] = redirect_to 14 | if form: 15 | if request.method == "POST": 16 | if form.is_valid(): 17 | status = 200 18 | else: 19 | status = 400 20 | else: 21 | status = 200 22 | resp["form"] = self.ajax_response_form(form) 23 | if data is not None: 24 | resp["data"] = data 25 | return JsonResponse(resp, status=status) 26 | -------------------------------------------------------------------------------- /backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/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", "backend.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danjac/django-vue-auth-demo/14440162986e0c2451303e25d52d6fca55d180d0/backend/posts/__init__.py -------------------------------------------------------------------------------- /backend/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = "posts" 6 | -------------------------------------------------------------------------------- /backend/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-20 10:14 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="Post", 19 | fields=[ 20 | ( 21 | "id", 22 | models.AutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("title", models.CharField(max_length=300)), 30 | ("url", models.URLField(blank=True, null=True)), 31 | ("description", models.TextField(blank=True)), 32 | ("created", models.DateTimeField(auto_now_add=True)), 33 | ( 34 | "author", 35 | models.ForeignKey( 36 | on_delete=django.db.models.deletion.CASCADE, 37 | to=settings.AUTH_USER_MODEL, 38 | ), 39 | ), 40 | ], 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /backend/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danjac/django-vue-auth-demo/14440162986e0c2451303e25d52d6fca55d180d0/backend/posts/migrations/__init__.py -------------------------------------------------------------------------------- /backend/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | 5 | class Post(models.Model): 6 | title = models.CharField(max_length=300) 7 | url = models.URLField(null=True, blank=True) 8 | description = models.TextField(blank=True) 9 | created = models.DateTimeField(auto_now_add=True) 10 | author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 11 | -------------------------------------------------------------------------------- /backend/posts/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsPostAuthorOrReadOnly(permissions.BasePermission): 5 | def has_object_permission(self, request, view, obj): 6 | if request.method in permissions.SAFE_METHODS: 7 | return True 8 | return obj.author == request.user 9 | -------------------------------------------------------------------------------- /backend/posts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Post 4 | 5 | 6 | class PostSerializer(serializers.ModelSerializer): 7 | 8 | author = serializers.StringRelatedField() 9 | 10 | class Meta: 11 | model = Post 12 | fields = ("id", "title", "url", "description", "author", "created") 13 | read_only_fields = ("author",) 14 | -------------------------------------------------------------------------------- /backend/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/posts/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions, viewsets 2 | 3 | from .models import Post 4 | from .permissions import IsPostAuthorOrReadOnly 5 | from .serializers import PostSerializer 6 | 7 | 8 | class PostViewSet(viewsets.ModelViewSet): 9 | 10 | queryset = Post.objects.select_related("author").order_by("-created") 11 | serializer_class = PostSerializer 12 | permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsPostAuthorOrReadOnly] 13 | 14 | def perform_create(self, serializer): 15 | serializer.save(author=self.request.user) 16 | -------------------------------------------------------------------------------- /backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "h5b*wcx2p7)($7$33ka^+!!)@zy$#4vm_r4qu(#rbr^w(66$#b" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.sites", 40 | # place above staticfiles 41 | "whitenoise.runserver_nostatic", 42 | "django.contrib.staticfiles", 43 | "allauth", 44 | "allauth.account", 45 | "rest_framework", 46 | "backend.posts", 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | "django.middleware.security.SecurityMiddleware", 51 | "whitenoise.middleware.WhiteNoiseMiddleware", 52 | "django.contrib.sessions.middleware.SessionMiddleware", 53 | "django.middleware.common.CommonMiddleware", 54 | "django.middleware.csrf.CsrfViewMiddleware", 55 | "django.contrib.auth.middleware.AuthenticationMiddleware", 56 | "django.contrib.messages.middleware.MessageMiddleware", 57 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 58 | ] 59 | 60 | ROOT_URLCONF = "backend.urls" 61 | 62 | TEMPLATES = [ 63 | { 64 | "BACKEND": "django.template.backends.django.DjangoTemplates", 65 | "DIRS": ["dist", "templates"], 66 | "APP_DIRS": True, 67 | "OPTIONS": { 68 | "context_processors": [ 69 | "django.template.context_processors.debug", 70 | "django.template.context_processors.request", 71 | "django.contrib.auth.context_processors.auth", 72 | "django.contrib.messages.context_processors.messages", 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = "backend.wsgi.application" 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 83 | 84 | DATABASES = { 85 | "default": { 86 | "ENGINE": "django.db.backends.sqlite3", 87 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 98 | }, 99 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 100 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 101 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 107 | 108 | LANGUAGE_CODE = "en-us" 109 | 110 | TIME_ZONE = "UTC" 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 121 | 122 | STATIC_URL = "/static/" 123 | 124 | STATIC_ROOT = os.path.join(BASE_DIR, "dist", "static") 125 | 126 | STATICFILES_DIRS = [] 127 | STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" 128 | 129 | SITE_ID = 1 130 | 131 | # ACCOUNT_ADAPTER = "backend.adapters.CustomAccountAdapter" 132 | 133 | REST_FRAMEWORK = { 134 | "DEFAULT_AUTHENTICATION_CLASSES": [ 135 | "rest_framework.authentication.SessionAuthentication", 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /backend/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path, re_path 3 | 4 | from rest_framework import routers 5 | 6 | from .posts.views import PostViewSet 7 | from .views import current_user_view, index_view 8 | 9 | router = routers.DefaultRouter() 10 | router.register("posts", PostViewSet) 11 | 12 | urlpatterns = [ 13 | path("api/me/", current_user_view), 14 | path("api/auth/", include("allauth.urls")), 15 | path("api/", include(router.urls)), 16 | path("admin/", admin.site.urls), 17 | # let vue-router handle any other routes internally 18 | re_path(r"^.*", index_view), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import get_user_model 3 | from django.views.decorators.cache import never_cache 4 | from django.views.decorators.csrf import ensure_csrf_cookie 5 | from django.views.generic import TemplateView 6 | 7 | from rest_framework import generics, permissions, serializers 8 | 9 | 10 | class IndexView(TemplateView): 11 | """Boots up the view in production.""" 12 | 13 | template_name = "index.html" 14 | 15 | # grab all server rendered messages 16 | def get_messages(self): 17 | return [ 18 | {"message": message.message, "level": message.level} 19 | for message in messages.get_messages(self.request) 20 | ] 21 | 22 | def get_context_data(self, **kwargs): 23 | data = super().get_context_data(**kwargs) 24 | data["init_json"] = { 25 | "current_user": UserSerializer(self.request.user).data, 26 | "messages": self.get_messages(), 27 | } 28 | return data 29 | 30 | 31 | index_view = never_cache(ensure_csrf_cookie(IndexView.as_view())) 32 | 33 | 34 | class UserSerializer(serializers.ModelSerializer): 35 | class Meta: 36 | model = get_user_model() 37 | fields = ("username", "email", "is_authenticated") 38 | 39 | 40 | class CurrentUserView(generics.RetrieveAPIView): 41 | serializer_class = UserSerializer 42 | permissions = [permissions.IsAuthenticated] 43 | 44 | def get_object(self): 45 | return self.request.user 46 | 47 | 48 | current_user_view = CurrentUserView.as_view() 49 | -------------------------------------------------------------------------------- /backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/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", "backend.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /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', 'backend.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-vue-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service build --mode development --watch", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.19.2", 12 | "core-js": "^3.6.4", 13 | "vue": "^2.6.11", 14 | "vue-router": "^3.1.6", 15 | "vuex": "^3.1.3" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "~4.3.0", 19 | "@vue/cli-plugin-eslint": "~4.3.0", 20 | "@vue/cli-plugin-router": "~4.3.0", 21 | "@vue/cli-plugin-vuex": "~4.3.0", 22 | "@vue/cli-service": "~4.3.0", 23 | "@vue/eslint-config-prettier": "^6.0.0", 24 | "autoprefixer": "^9.8.0", 25 | "babel-eslint": "^10.1.0", 26 | "eslint": "^6.7.2", 27 | "eslint-plugin-prettier": "^3.1.1", 28 | "eslint-plugin-vue": "^6.2.2", 29 | "prettier": "^1.19.1", 30 | "tailwindcss": "^1.4.6", 31 | "vue-template-compiler": "^2.6.11" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "ASGI specs, helper code, and adapters" 4 | name = "asgiref" 5 | optional = false 6 | python-versions = ">=3.5" 7 | version = "3.2.7" 8 | 9 | [package.extras] 10 | tests = ["pytest (>=4.3.0,<4.4.0)", "pytest-asyncio (>=0.10.0,<0.11.0)"] 11 | 12 | [[package]] 13 | category = "main" 14 | description = "Python package for providing Mozilla's CA Bundle." 15 | name = "certifi" 16 | optional = false 17 | python-versions = "*" 18 | version = "2020.4.5.2" 19 | 20 | [[package]] 21 | category = "main" 22 | description = "Universal encoding detector for Python 2 and 3" 23 | name = "chardet" 24 | optional = false 25 | python-versions = "*" 26 | version = "3.0.4" 27 | 28 | [[package]] 29 | category = "main" 30 | description = "XML bomb protection for Python stdlib modules" 31 | name = "defusedxml" 32 | optional = false 33 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 34 | version = "0.6.0" 35 | 36 | [[package]] 37 | category = "main" 38 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 39 | name = "django" 40 | optional = false 41 | python-versions = ">=3.6" 42 | version = "3.0.7" 43 | 44 | [package.dependencies] 45 | asgiref = ">=3.2,<4.0" 46 | pytz = "*" 47 | sqlparse = ">=0.2.2" 48 | 49 | [package.extras] 50 | argon2 = ["argon2-cffi (>=16.1.0)"] 51 | bcrypt = ["bcrypt"] 52 | 53 | [[package]] 54 | category = "main" 55 | description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." 56 | name = "django-allauth" 57 | optional = false 58 | python-versions = "*" 59 | version = "0.41.0" 60 | 61 | [package.dependencies] 62 | Django = ">=2.0" 63 | python3-openid = ">=3.0.8" 64 | requests = "*" 65 | requests-oauthlib = ">=0.3.0" 66 | 67 | [[package]] 68 | category = "main" 69 | description = "Web APIs for Django, made easy." 70 | name = "djangorestframework" 71 | optional = false 72 | python-versions = ">=3.5" 73 | version = "3.11.0" 74 | 75 | [package.dependencies] 76 | django = ">=1.11" 77 | 78 | [[package]] 79 | category = "main" 80 | description = "Internationalized Domain Names in Applications (IDNA)" 81 | name = "idna" 82 | optional = false 83 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 84 | version = "2.9" 85 | 86 | [[package]] 87 | category = "main" 88 | description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" 89 | name = "oauthlib" 90 | optional = false 91 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 92 | version = "3.1.0" 93 | 94 | [package.extras] 95 | rsa = ["cryptography"] 96 | signals = ["blinker"] 97 | signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] 98 | 99 | [[package]] 100 | category = "main" 101 | description = "OpenID support for modern servers and consumers." 102 | name = "python3-openid" 103 | optional = false 104 | python-versions = "*" 105 | version = "3.1.0" 106 | 107 | [package.dependencies] 108 | defusedxml = "*" 109 | 110 | [[package]] 111 | category = "main" 112 | description = "World timezone definitions, modern and historical" 113 | name = "pytz" 114 | optional = false 115 | python-versions = "*" 116 | version = "2020.1" 117 | 118 | [[package]] 119 | category = "main" 120 | description = "Python HTTP for Humans." 121 | name = "requests" 122 | optional = false 123 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 124 | version = "2.23.0" 125 | 126 | [package.dependencies] 127 | certifi = ">=2017.4.17" 128 | chardet = ">=3.0.2,<4" 129 | idna = ">=2.5,<3" 130 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 131 | 132 | [package.extras] 133 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 134 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] 135 | 136 | [[package]] 137 | category = "main" 138 | description = "OAuthlib authentication support for Requests." 139 | name = "requests-oauthlib" 140 | optional = false 141 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 142 | version = "1.3.0" 143 | 144 | [package.dependencies] 145 | oauthlib = ">=3.0.0" 146 | requests = ">=2.0.0" 147 | 148 | [package.extras] 149 | rsa = ["oauthlib (>=3.0.0)"] 150 | 151 | [[package]] 152 | category = "main" 153 | description = "Non-validating SQL parser" 154 | name = "sqlparse" 155 | optional = false 156 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 157 | version = "0.3.1" 158 | 159 | [[package]] 160 | category = "main" 161 | description = "HTTP library with thread-safe connection pooling, file post, and more." 162 | name = "urllib3" 163 | optional = false 164 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 165 | version = "1.25.9" 166 | 167 | [package.extras] 168 | brotli = ["brotlipy (>=0.6.0)"] 169 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] 170 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 171 | 172 | [[package]] 173 | category = "main" 174 | description = "Radically simplified static file serving for WSGI applications" 175 | name = "whitenoise" 176 | optional = false 177 | python-versions = ">=3.5, <4" 178 | version = "5.1.0" 179 | 180 | [package.extras] 181 | brotli = ["brotli"] 182 | 183 | [metadata] 184 | content-hash = "5af306e84b63dc4aa0c550768864ded7dc9d7bd20399d35570a6ed62b0a39111" 185 | python-versions = "^3.8" 186 | 187 | [metadata.files] 188 | asgiref = [ 189 | {file = "asgiref-3.2.7-py2.py3-none-any.whl", hash = "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"}, 190 | {file = "asgiref-3.2.7.tar.gz", hash = "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5"}, 191 | ] 192 | certifi = [ 193 | {file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"}, 194 | {file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"}, 195 | ] 196 | chardet = [ 197 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 198 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 199 | ] 200 | defusedxml = [ 201 | {file = "defusedxml-0.6.0-py2.py3-none-any.whl", hash = "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93"}, 202 | {file = "defusedxml-0.6.0.tar.gz", hash = "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"}, 203 | ] 204 | django = [ 205 | {file = "Django-3.0.7-py3-none-any.whl", hash = "sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8"}, 206 | {file = "Django-3.0.7.tar.gz", hash = "sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2"}, 207 | ] 208 | django-allauth = [ 209 | {file = "django-allauth-0.41.0.tar.gz", hash = "sha256:7ab91485b80d231da191d5c7999ba93170ef1bf14ab6487d886598a1ad03e1d8"}, 210 | ] 211 | djangorestframework = [ 212 | {file = "djangorestframework-3.11.0-py3-none-any.whl", hash = "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4"}, 213 | {file = "djangorestframework-3.11.0.tar.gz", hash = "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f"}, 214 | ] 215 | idna = [ 216 | {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, 217 | {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, 218 | ] 219 | oauthlib = [ 220 | {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"}, 221 | {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"}, 222 | ] 223 | python3-openid = [ 224 | {file = "python3-openid-3.1.0.tar.gz", hash = "sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502"}, 225 | {file = "python3_openid-3.1.0-py3-none-any.whl", hash = "sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa"}, 226 | ] 227 | pytz = [ 228 | {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, 229 | {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, 230 | ] 231 | requests = [ 232 | {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, 233 | {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, 234 | ] 235 | requests-oauthlib = [ 236 | {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, 237 | {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, 238 | {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, 239 | ] 240 | sqlparse = [ 241 | {file = "sqlparse-0.3.1-py2.py3-none-any.whl", hash = "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e"}, 242 | {file = "sqlparse-0.3.1.tar.gz", hash = "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"}, 243 | ] 244 | urllib3 = [ 245 | {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, 246 | {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, 247 | ] 248 | whitenoise = [ 249 | {file = "whitenoise-5.1.0-py2.py3-none-any.whl", hash = "sha256:6dd26bfda3af29177d8ab7333a0c7b7642eb615ce83764f4d15a9aecda3201c4"}, 250 | {file = "whitenoise-5.1.0.tar.gz", hash = "sha256:60154b976a13901414a25b0273a841145f77eb34a141f9ae032a0ace3e4d5b27"}, 251 | ] 252 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": [ 3 | require('tailwindcss')('tailwind.config.js'), 4 | require('autoprefixer')(), 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danjac/django-vue-auth-demo/14440162986e0c2451303e25d52d6fca55d180d0/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample App 5 | 6 | 7 | {{ init_json|json_script:"init-data" }} 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "backend" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Dan Jacob "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | django = "^3.0.6" 10 | djangorestframework = "^3.11.0" 11 | django-allauth = "^0.41.0" 12 | whitenoise = "^5.0.1" 13 | 14 | [tool.poetry.dev-dependencies] 15 | 16 | [build-system] 17 | requires = ["poetry>=0.12"] 18 | build-backend = "poetry.masonry.api" 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 64 | -------------------------------------------------------------------------------- /src/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danjac/django-vue-auth-demo/14440162986e0c2451303e25d52d6fca55d180d0/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 113 | 114 | 115 | 131 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import Vue from "vue"; 3 | import App from "./App.vue"; 4 | import router from "./router"; 5 | import store from "./store"; 6 | 7 | import "@/assets/css/main.css"; 8 | 9 | Vue.config.productionTip = false; 10 | 11 | axios.defaults.xsrfHeaderName = "X-CSRFToken"; 12 | axios.defaults.xsrfCookieName = "csrftoken"; 13 | axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; 14 | 15 | // check if user logged in 16 | // also for production: user data (and any other useful stuff) 17 | // could be serialized into the template as a JSON snippet 18 | // and we can check this on page load. 19 | 20 | // this is spaghetti but basically: check if init data in index.html, 21 | // if not load from API. 22 | (function() { 23 | const dataTag = document.getElementById("init-data"); 24 | if (dataTag) { 25 | const initData = JSON.parse(dataTag.textContent); 26 | if (initData.current_user.is_authenticated) { 27 | store.dispatch("authenticate", initData.current_user); 28 | } else { 29 | store.dispatch("logout"); 30 | } 31 | if (initData.messages) { 32 | store.dispatch("addMessages", initData.messages); 33 | } 34 | } 35 | 36 | return new Vue({ 37 | router, 38 | store, 39 | render: h => h(App) 40 | }).$mount("#app"); 41 | })(); 42 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import store from "../store"; 4 | import Home from "../views/Home.vue"; 5 | 6 | Vue.use(VueRouter); 7 | 8 | const routes = [ 9 | { 10 | path: "/", 11 | name: "Home", 12 | component: Home 13 | }, 14 | { 15 | path: "/about", 16 | name: "About", 17 | // route level code-splitting 18 | // this generates a separate chunk (about.[hash].js) for this route 19 | // which is lazy-loaded when the route is visited. 20 | component: () => 21 | import(/* webpackChunkName: "about" */ "../views/About.vue") 22 | }, 23 | { 24 | path: "/login", 25 | name: "Login", 26 | component: () => 27 | import(/* webpackChunkName: "login" */ "../views/Login.vue") 28 | }, 29 | { 30 | path: "/signup", 31 | name: "Signup", 32 | component: () => 33 | import(/* webpackChunkName: "signup" */ "../views/Signup.vue") 34 | }, 35 | { 36 | path: "/submit", 37 | name: "SubmitPost", 38 | component: () => 39 | import(/* webpackChunkName: "submitPost" */ "../views/SubmitPost.vue"), 40 | meta: { 41 | requireAuth: true 42 | } 43 | }, 44 | { 45 | path: "/post/:id", 46 | name: "PostDetail", 47 | component: () => 48 | import(/* webpackChunkName: "postDetail" */ "../views/PostDetail.vue") 49 | }, 50 | { 51 | path: "/post/:id/~update", 52 | name: "PostUpdate", 53 | component: () => 54 | import(/* webpackChunkName: "postUpdate" */ "../views/PostUpdate.vue"), 55 | meta: { 56 | requireAuth: true 57 | } 58 | }, 59 | { 60 | path: "*", 61 | component: () => 62 | import(/* webpackChunkName: "notFound" */ "../views/NotFound.vue") 63 | } 64 | ]; 65 | 66 | const router = new VueRouter({ 67 | mode: "history", 68 | base: process.env.BASE_URL, 69 | routes 70 | }); 71 | 72 | router.beforeEach((to, from, next) => { 73 | if (to.matched.some(record => record.meta.requireAuth)) { 74 | if (store.getters.isLoggedIn) { 75 | next(); 76 | } else { 77 | next({ 78 | path: "/login", 79 | params: { nextUrl: to.fullPath } 80 | }); 81 | } 82 | } else { 83 | next(); 84 | } 85 | }); 86 | 87 | export default router; 88 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | currentUser: null, 9 | messages: [] 10 | }, 11 | mutations: { 12 | setCurrentUser(state, user) { 13 | state.currentUser = user; 14 | }, 15 | addMessages(state, messages) { 16 | state.messages = messages; 17 | }, 18 | addMessage(state, message) { 19 | state.messages.push(message); 20 | } 21 | }, 22 | actions: { 23 | authenticate({ commit }, user) { 24 | commit("setCurrentUser", user); 25 | }, 26 | logout({ commit }) { 27 | commit("setCurrentUser", null); 28 | }, 29 | addMessages({ commit }, messages) { 30 | commit("addMessages", messages); 31 | }, 32 | addMessage({ commit }, message) { 33 | commit("addMessage", message); 34 | }, 35 | clearMessages({ commit }) { 36 | commit("addMessages", []); 37 | } 38 | }, 39 | getters: { 40 | isLoggedIn(state) { 41 | return !!state.currentUser; 42 | }, 43 | isOwner(state) { 44 | return post => 45 | !!state.currentUser && post.author === state.currentUser.username; 46 | } 47 | }, 48 | modules: {} 49 | }); 50 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 71 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 76 | -------------------------------------------------------------------------------- /src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/PostDetail.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 88 | -------------------------------------------------------------------------------- /src/views/PostUpdate.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 100 | -------------------------------------------------------------------------------- /src/views/Signup.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 122 | -------------------------------------------------------------------------------- /src/views/SubmitPost.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 79 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { 3 | content: ["./src/**/*.vue"] 4 | }, 5 | theme: { 6 | extend: {} 7 | }, 8 | variants: {}, 9 | plugins: [] 10 | }; 11 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | outputDir: "dist", 3 | assetsDir: "static", 4 | }; 5 | --------------------------------------------------------------------------------