Skip to content

Commit 0127ed3

Browse files
committed
auth
1 parent 395954b commit 0127ed3

File tree

3 files changed

+200
-37
lines changed

3 files changed

+200
-37
lines changed

README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
- [Lazy Loading](#lazy-loading)
3535
- [API Communication](#api-communication)
3636
- [Receiving Data](#receiving-data)
37+
- [JWT](#jwt)
38+
- [Authentication Call](#authentication-call)
39+
- [Authentication Status](#authentication-status)
40+
- [Intercept Requests](#intercept-requests)
3741

3842
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
3943

@@ -703,3 +707,72 @@ Note Vue automatically sanitizes html. If html is being returned from an api and
703707
<h3 slot="title" v-html="post.title.rendered"></h3>
704708
<span slot="content" v-html="post.excerpt.rendered"></span>
705709
```
710+
711+
### JWT
712+
713+
Will add login feature so (authenticated) users can post data to the server. Will use JWT (json web token).
714+
715+
User sends username and password to server, which if valid, responds with tokenExpiration and a token.
716+
717+
User then sends this token along with every subsequent request.
718+
719+
Server generates token with a secret key. i.e. user an only read data sent, but cannot create own token or edit it because doesn't know the secret key.
720+
721+
Add login method to service layer than will resolve or reject.
722+
723+
### Authentication Call
724+
725+
In script of Login component, import appService.
726+
727+
Bind data to template using `v-model` directive, so that every time input values change (from user typing in form), the corresponding parameters in the Vue data will be updated.
728+
729+
```javascript
730+
import appService from '../app-service.js'
731+
export default {
732+
data() {
733+
return {
734+
username: '',
735+
password: ''
736+
}
737+
}
738+
}
739+
```
740+
741+
```html
742+
<input v-model="username" class="input" type="text" placeholder="Your username">
743+
<input v-model="password" class="input" type="password" placeholder="Your password">
744+
```
745+
746+
When user clicks login button, run the component's login method, using `v-on:` directive and the `click` parameter:
747+
748+
```html
749+
<button v-on:click="login" class="button is-primary">Login</button>
750+
```
751+
752+
```javascript
753+
methods: {
754+
login () {
755+
appService.login({username: this.username, password: this.password})
756+
.then((data) => {
757+
window.localStorage.setItem('token', data.token)
758+
window.localStorage.setItem('tokenExpiration', data.expiration)
759+
})
760+
.catch(() => window.alert('Could not login!'))
761+
}
762+
}
763+
```
764+
765+
Test with `bill/vuejs`
766+
767+
### Authentication Status
768+
769+
Add another property to data section of Login `isAuthenticated`. Populate it in `created` method, setting to true if expiration token exists in local storage and its in the future relative to current date.
770+
771+
Then in template, use `v-if` directive to only display a welcome message if user is authenticated.
772+
773+
For better security, add CSRF token and captcha to discourage bots (not covered in this course).
774+
775+
776+
### Intercept Requests
777+
778+
Add `getProfile` method to service layer, and watcher for `isAuthenticated` in Login.vue. When `isAuthenticated` becomes true, run `getProfile` method. However, will get 401 from server because the jwt token was not included as a request header. Add axios request interceptor to always add it.

src/app.service.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ import axios from 'axios'
22

33
axios.defaults.baseURL = 'https://api.fullstackweekly.com'
44

5+
axios.interceptors.request.use(function (config) {
6+
// future server side rendering - will not have window object so exit early
7+
if (typeof window === 'undefined') {
8+
return config
9+
}
10+
const token = window.localStorage.getItem('token')
11+
if (token) {
12+
config.headers.Authorization = `Bearer ${window.localStorage.getItem('token')}`
13+
}
14+
return config
15+
})
16+
517
const appService = {
618
getPosts (categoryId) {
719
return new Promise((resolve) => {
@@ -10,6 +22,24 @@ const appService = {
1022
resolve(response.data)
1123
})
1224
})
25+
},
26+
login (credentials) {
27+
return new Promise((resolve, reject) => {
28+
axios.post('/services/auth.php', credentials)
29+
.then(response => {
30+
resolve(response.data)
31+
}).catch(response => {
32+
reject(response.status)
33+
})
34+
})
35+
},
36+
getProfile () {
37+
return new Promise((resolve) => {
38+
axios.get('/services/profile.php')
39+
.then(response => {
40+
resolve(response.data)
41+
})
42+
})
1343
}
1444
}
1545

src/theme/Login.vue

Lines changed: 97 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,101 @@
11
<template>
22
<div class="content">
3-
<h2>Login</h2>
4-
<div class="field is-horizontal">
5-
<div class="field-label is-normal">
6-
<label class="label">Username</label>
7-
</div>
8-
<div class="field-body">
9-
<div class="field">
10-
<div class="control">
11-
<input class="input" type="text" placeholder="Your username">
12-
</div>
13-
</div>
14-
</div>
15-
</div>
16-
<div class="field is-horizontal">
17-
<div class="field-label is-normal">
18-
<label class="label">Password</label>
19-
</div>
20-
<div class="field-body">
21-
<div class="field">
22-
<div class="control">
23-
<input class="input" type="password" placeholder="Your password">
24-
</div>
25-
</div>
26-
</div>
27-
</div>
28-
<div class="field is-horizontal">
29-
<div class="field-label">
30-
<!-- Left empty for spacing -->
31-
</div>
32-
<div class="field-body">
33-
<div class="field">
34-
<div class="control">
35-
<button class="button is-primary">Login</button>
36-
</div>
37-
</div>
38-
</div>
39-
</div>
3+
<div v-if="isAuthenticated">
4+
Hello authenticated user!
5+
<p>Name: {{profile.firstName}}</p>
6+
<p>Favorite Sandwich: {{profile.favoriteSandwich}}</p>
7+
<button v-on:click="logout" class="button is-primary">Logout</button>
8+
</div>
9+
<div v-else>
10+
<h2>Login</h2>
11+
<div class="field is-horizontal">
12+
<div class="field-label is-normal">
13+
<label class="label">Username</label>
14+
</div>
15+
<div class="field-body">
16+
<div class="field">
17+
<div class="control">
18+
<input v-model="username" class="input" type="text" placeholder="Your username">
19+
</div>
20+
</div>
21+
</div>
22+
</div>
23+
<div class="field is-horizontal">
24+
<div class="field-label is-normal">
25+
<label class="label">Password</label>
26+
</div>
27+
<div class="field-body">
28+
<div class="field">
29+
<div class="control">
30+
<input v-model="password" class="input" type="password" placeholder="Your password">
31+
</div>
32+
</div>
33+
</div>
34+
</div>
35+
<div class="field is-horizontal">
36+
<div class="field-label">
37+
<!-- Left empty for spacing -->
38+
</div>
39+
<div class="field-body">
40+
<div class="field">
41+
<div class="control">
42+
<button v-on:click="login" class="button is-primary">Login</button>
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
</div>
4048
</div>
4149
</template>
50+
51+
<script>
52+
import appService from '../app.service.js'
53+
export default {
54+
data () {
55+
return {
56+
username: '',
57+
password: '',
58+
isAuthenticated: false,
59+
profile: {}
60+
}
61+
},
62+
watch: {
63+
isAuthenticated: function (val) {
64+
if (val) {
65+
appService.getProfile()
66+
.then(profile => {
67+
this.profile = profile
68+
})
69+
} else {
70+
this.profile = {}
71+
}
72+
}
73+
},
74+
methods: {
75+
login () {
76+
appService.login({username: this.username, password: this.password})
77+
.then((data) => {
78+
window.localStorage.setItem('token', data.token)
79+
window.localStorage.setItem('tokenExpiration', data.expiration)
80+
this.isAuthenticated = true
81+
// make sure login form will be empty if user wants to log in again
82+
this.username = ''
83+
this.password = ''
84+
})
85+
.catch(() => window.alert('Could not login!'))
86+
},
87+
logout () {
88+
window.localStorage.setItem('token', null)
89+
window.localStorage.setItem('tokenExpiration', null)
90+
this.isAuthenticated = false
91+
}
92+
},
93+
created () {
94+
let expiration = window.localStorage.getItem('tokenExpiration')
95+
let unixTimestamp = new Date().getTime() / 1000
96+
if (expiration !== null && parseInt(expiration) - unixTimestamp > 0) {
97+
this.isAuthenticated = true
98+
}
99+
}
100+
}
101+
</script>

0 commit comments

Comments
 (0)