Intergate Vue in Django

Intergate Vue in Django

What Project Will We Build

We want to build a vue project with multiple pages. The Django view render these pages as templates.

Why We Build Such A Project

If we seperate frond-end and back-end and all routers are handled by the front-end project. The attackers can fake the reponse of authentication and jump to the target pages. If the attackers get the front-end code all in one package without authentication, they can parse out API easily.

So we consider to use front-end service to control the router and use back-end for access control.

Initial Repository

1
2
mkdir vid && cd vid
git init

Create Virtual Environment

Specify Python Version

1
pipenv --python ~/.pyenv/versions/3.7.1/bin/python3.7

Change Mirror (Optional)

1
2
3
4
[[source]]
name = "pypi"
url = "https://mirrors.aliyun.com/pypi/simple/"
verify_ssl = true

Install Dependencies

1
pipenv install django==2.2.1

Start Django Project

1
2
3
pipenv run django-admin startproject vid
mv vid vid-dir && mv ./vid-dir/* . && rmdir vid-dir # move project to current path
pipenv run python manage.py runserver 127.0.0.1:8000 # check django project works

Create Authorization API in Django

You should always create a custom user model in Django. Here is why.

Remember not migrating until the custom user model is registered correctly.

Create Custom User Model

First, let us create an app.

1
pipenv run python manage.py startapp account

We must install it in INSTALLED_APPS in settings.

1
2
3
4
5
6
# vid/settings.py

INSTALLED_APPS = [
# ...
'account', # install account app
]

Then we need to create a custom user model.

1
2
3
4
5
6
7
8
# account/model.py

from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
pass

And replace the default one by registering it.

1
2
3
# vid/settings.py

AUTH_USER_MODEL = 'account.User'

After all, migrating the user model.

1
2
pipenv run python manage.py makemigrations
pipenv run python manage.py migrate

Create Logining and Checking Views

Let’s create class-based views with json responses.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# account/apis.py

import json

from django.contrib.auth import authenticate, login, logout
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.views import View


class LoginView(View):

def post(self, request: HttpRequest) -> JsonResponse:
body = json.loads(request.body) # TODO: request validation
user = authenticate(**body)
if user is None:
return JsonResponse(dict(status='error', message='wrong password'))
login(request, user)
return JsonResponse(dict(status='success'))


class GetUserInfoView(View):

def get(self, request: HttpRequest) -> JsonResponse:
user = request.user
if not user.is_authenticated:
return JsonResponse(dict(status='error', message='not login'))
return JsonResponse(dict(status='success',data=dict(username=user.username)))


class LogoutView(View):

def get(self, request: HttpRequest) -> JsonResponse:
user = request.user
if not user.is_authenticated:
return JsonResponse(dict(status='error', message='not login'))

logout(request)
return JsonResponse(dict(status='success'))

Then, make router for it.

1
2
3
4
5
6
7
8
9
10
# account/urls.py

from django.urls import include, path
from .apis import LoginView, LogoutView, GetUserInfoView

urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path('get_user_info/', GetUserInfoView.as_view(), name='get_user_info'),
]
1
2
3
4
5
6
7
8
9
# vid/urls.py

# ----- snip -----
from django.urls import include, path

urlpatterns = [
# ----- snip -----
path('api/account/', include('account.urls')),
]

Creating Vue Project with Multiple Pages

Initializing a Vue Project

Frist of all, let us initialize a vue project.

1
2
3
vue create frontend
cd frontend
yarn install

Then we run the vue development service.

1
yarn run serve

Open the URL show in terminal specified by our vue service. Make sure the vue project work well on it (usually localhost:8080).

Satisfication for Multiple Pages

Move the entry into appropriate path.

1
2
3
4
# in the frontend directory
mkdir -p src/pages/login
mv main.js src/pages/login/app.js
mv App.vue src/pages/login/app.vue

Change relative path in app.vue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- src/pages/login/app.vue -->

<template>
<div id="app">
<img alt="Vue logo" src="../../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>

<script>
import HelloWorld from '../../components/HelloWorld.vue'

export default {
// remove the 'name' field
components: {
HelloWorld
}
}
</script>

<!-- snipped -->

Copy a new page named “user”.

1
cp -r src/pages/login src/pages/user

Edit parameter msg in src/pages/user to make difference between pages.

1
2
3
4
<!-- snipped -->
<HelloWorld msg="This Is The User Page"/>
<!-- snipped -->

Create vue.config.js for build multiple pages.

1
2
# in the frontend directory
touch vue.config.js

Parsing and registering all pages in config file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'use strict'
const titles = require('./src/titles.js')
const glob = require('glob')
const pages = {}

glob.sync('./src/pages/**/app.js').forEach(path => {
const chunk = path.split('./src/pages/')[1].split('/app.js')[0]
pages[chunk] = {
entry: path,
template: 'public/index.html',
title: titles[chunk],
chunks: ['chunk-vendors', 'chunk-common', chunk]
}
})

module.exports = {
pages,
chainWebpack: config => config.plugins.delete('named-chunks'),
}

Run the service again.

1
yarn run serve

Now you can view the login and user pages corresponding url, such as localhost:8080/login/ and localhost:8080/user/.

Login page:

image-20190529202025524

User page:

image-20190529202405290

Inject Vue Page Into Django Template

Build Vue Dist

Build the vue project first.

1
yarn run build

And now, you get login.html and user.html in the path frontend/dist.

Show Login Page in Django View

Let us show login page by TemplateView in Django.

1
2
3
4
5
6
7
8
9
10
# vid/urls.py

# ----- snipped -----
from django.views.generic import TemplateView

# ----- snipped -----
urlpatterns = [
# ----- snipped -----
path('login/', TemplateView.as_view(template_name='login.html')),
]

Change template directory in settings.

1
2
3
4
5
6
7
8
9
10
# vid/settings.py

# ----- snipped -----
TEMPLATES = [
{
# ----- snipped -----
'DIRS': [os.path.join(BASE_DIR, 'frontend', 'dist')],
# ----- snipped -----
},
]

After Changing template path, the login view works well. But we find the static files is not found.

image-20190529205044695

We setting assetsDir as 'static' in vue.config.js will ask vue-cli to build all assets in the same directory frontend/dist/static.

1
2
3
4
5
6
7
// frontend/vue.config.js

// ----- snipped -----
module.exports = {
assetsDir: 'static',
// ----- snipped -----
}

And then, registering the path in the Django settings.

1
2
3
4
5
# vid/settings.py

STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'frontend/dist/static'),
]

Now, we can rebuild the vue project and refresh the page.

1
yarn run build

After that, all files can be load correctly.

image-20190529210423475

Render User Page

Now, we already have the HTML file frontend/dist/user.html . Let us render it in a Django view.

1
2
3
4
5
6
7
8
9
10
11
12
13
# account/views.py

from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from django.views import View


class UserPageView(View):
def get(self, request: HttpRequest) -> HttpResponse:
if not request.user.is_authenticated:
return HttpResponseRedirect(reverse('login_page'))
return render(request, template_name='user.html')
1
2
3
4
5
6
7
# vid/urls.py

urlpatterns = [
# ----- snipped -----
path('user/', UserPageView.as_view(), name='user_page'),
# ----- snipped -----
]

Let us try browsing the URL of user page (usually localhost:8000/user). Focus on the network tracks. We can see the request is redirected to the login page because we did not log in.

image-20190530095235106

Create a Login Form in User Page

Login Form with Ant Design

First, let us install the ant design vue package. Here is the way to install and register ant design vue.

1
yarn add ant-design-vue

Import style in frontend/src/pages/login/app.js

1
import "ant-design-vue/dist/antd.css";

Create a new file in frontend/src/components, copy the login form from doc. Then:

  • import components from ant design vue
  • register components required
  • disable actions in mounted (optional)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# frontend/src/components/LoginForm.vue

<template>
<a-form
layout="inline"
:form="form"
@submit="handleSubmit"
>
<a-form-item
:validate-status="userNameError() ? 'error' : ''"
:help="userNameError() || ''"
>
<a-input
v-decorator="[
'userName',
{rules: [{ required: true, message: 'Please input your username!' }]}
]"
placeholder="Username"
>
<a-icon
slot="prefix"
type="user"
style="color:rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
:validate-status="passwordError() ? 'error' : ''"
:help="passwordError() || ''"
>
<a-input
v-decorator="[
'password',
{rules: [{ required: true, message: 'Please input your Password!' }]}
]"
type="password"
placeholder="Password"
>
<a-icon
slot="prefix"
type="lock"
style="color:rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:disabled="hasErrors(form.getFieldsError())"
>
Log in
</a-button>
</a-form-item>
</a-form>
</template>

<script>
import { Form, Button, Input, Icon } from 'ant-design-vue'
import ant from 'ant-design-vue'
console.log('ant', ant) // eslint-disable-line

function hasErrors (fieldsError) {
return Object.keys(fieldsError).some(field => fieldsError[field])
}

export default {
components: {
'a-button': Button,
'a-form': Form,
'a-form-item': Form.Item,
'a-input': Input,
'a-icon': Icon,
},
data () {
return {
hasErrors,
form: this.$form.createForm(this),
}
},
mounted () {
this.$nextTick(() => {
// To disabled submit button at the beginning.
// this.form.validateFields()
})
},
methods: {
// Only show error after a field is touched.
userNameError () {
const { getFieldError, isFieldTouched } = this.form
return isFieldTouched('userName') && getFieldError('userName')
},
// Only show error after a field is touched.
passwordError () {
const { getFieldError, isFieldTouched } = this.form
return isFieldTouched('password') && getFieldError('password')
},
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
},
},
}
</script>

Now we have a login form.

image-20190530115347454

Using Axios to Send Login Request

First let add proxy in vue project.

1
2
3
4
5
6
7
8
9
10
11
12
13
// frontend/vue.config.js

module.exports = {
# ----- snipped -----
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000', // the host of your django service
changeOrigin: true,
}
}
}
}

Install Axios.

1
yarn add axios

Have a try to request by Axios.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// frontend/src/components/LoginForm.vue

<script>
import axios from 'axios'
import { Form, Button, Input, Icon, message } from 'ant-design-vue'

// ----- snipped -----
export default {
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values) // eslint-disable-line
this.login(values.userName, values.password)
}
})
},
async login (username, password) {
let { data: { status, message: msg } } = await axios.post('/api/account/login/', {username, password})
if (status !== 'success') {
message.error(msg)
} else {
window.location = '/user/'
}
}
}
</script>

You will find CSRF token error.

image-20190530143518162

I do not find a better way to resolve it. Let disable CSRF token middleware until we find a solution.

1
2
3
4
5
6
7
# vid/settings.py

MIDDLEWARE = [
# ----- snipped -----
# 'django.middleware.csrf.CsrfViewMiddleware',
# ----- snipped -----
]

Create User and Test Redirection

Let us create a user in django shell.

1
pipenv run python manage.py shell
1
2
3
In [1]: from account.models import User
In [2]: User.objects.create_user(username='john', email='john@vid.com', password='weakpwd')
Out[2]: <User: john>

And now we can use the account on vue service (usually localhost:8080/login/) go to the user page (usually localhost:8080/user/).

image-20190530150502432

Rebuild Frontend and Have A Try in Django Service

Now let’s rebuild the frontend.

1
2
# in frontend directory
yarn run build

Now we can login and see the user page (localhost:8000/user/).

image-20190530153935383

Reference: