added (not yet working) code for login

This commit is contained in:
2019-03-14 17:13:24 +01:00
parent 18f22fdffa
commit 2cff1db5cb
17 changed files with 710 additions and 1 deletions

View File

@@ -6,6 +6,7 @@
</div>
<router-view/>
</div>
</template>
<style lang="scss">

View File

@@ -0,0 +1,11 @@
export function authHeader() {
// return authorization header with jwt token
// @ts-ignore
let user = JSON.parse(localStorage.getItem('user'));
if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token };
} else {
return {};
}
}

148
src/helpers/fake-backend.ts Normal file
View File

@@ -0,0 +1,148 @@
// array in local storage for registered users
// @ts-ignore
let users = JSON.parse(localStorage.getItem('users')) || [];
export function configureFakeBackend() {
let realFetch = window.fetch;
window.fetch = function (url, opts) {
return new Promise((resolve, reject) => {
// wrap in timeout to simulate server api call
setTimeout(() => {
// authenticate
// @ts-ignore
if (url.endsWith('/users/authenticate') && opts.method === 'POST') {
// get parameters from post request
// @ts-ignore
let params = JSON.parse(opts.body);
// find if any user matches login credentials
let filteredUsers = users.filter((user: any) => {
return user.username === params.username && user.password === params.password;
});
if (filteredUsers.length) {
// if login details are valid return user details and fake jwt token
let user = filteredUsers[0];
let responseJson = {
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
token: 'fake-jwt-token'
};
// @ts-ignore
resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(responseJson)) });
} else {
// else return error
reject('Username or password is incorrect');
}
return;
}
// get users
// @ts-ignore
if (url.endsWith('/users') && opts.method === 'GET') {
// check for fake auth token in header and return users if valid, this security is implemented server side in a real application
// @ts-ignore
if (opts.headers && opts.headers.Authorization === 'Bearer fake-jwt-token') {
// @ts-ignore
resolve({ ok: true, text: () => Promise.resolve(JSON.stringify(users))});
} else {
// return 401 not authorised if token is null or invalid
reject('Unauthorised');
}
return;
}
// get user by id
// @ts-ignore
if (url.match(/\/users\/\d+$/) && opts.method === 'GET') {
// check for fake auth token in header and return user if valid, this security is implemented server side in a real application
// @ts-ignore
if (opts.headers && opts.headers.Authorization === 'Bearer fake-jwt-token') {
// find user by id in users array
// @ts-ignore
let urlParts = url.split('/');
let id = parseInt(urlParts[urlParts.length - 1]);
// @ts-ignore
let matchedUsers = users.filter((user) => { return user.id === id; });
let user = matchedUsers.length ? matchedUsers[0] : null;
// respond 200 OK with user
// @ts-ignore
resolve({ ok: true, text: () => JSON.stringify(user)});
} else {
// return 401 not authorised if token is null or invalid
reject('Unauthorised');
}
return;
}
// register user
// @ts-ignore
if (url.endsWith('/users/register') && opts.method === 'POST') {
// get new user object from post body
// @ts-ignore
let newUser = JSON.parse(opts.body);
// validation
let duplicateUser = users.filter((user: any) => { return user.username === newUser.username; }).length;
if (duplicateUser) {
reject('Username "' + newUser.username + '" is already taken');
return;
}
// save new user
newUser.id = users.length ? Math.max(...users.map((user: any) => user.id)) + 1 : 1;
users.push(newUser);
localStorage.setItem('users', JSON.stringify(users));
// respond 200 OK
// @ts-ignore
resolve({ ok: true, text: () => Promise.resolve() });
return;
}
// delete user
// @ts-ignore
if (url.match(/\/users\/\d+$/) && opts.method === 'DELETE') {
// check for fake auth token in header and return user if valid, this security is implemented server side in a real application
// @ts-ignore
if (opts.headers && opts.headers.Authorization === 'Bearer fake-jwt-token') {
// find user by id in users array
// @ts-ignore
let urlParts = url.split('/');
let id = parseInt(urlParts[urlParts.length - 1]);
for (let i = 0; i < users.length; i++) {
let user = users[i];
if (user.id === id) {
// delete user
users.splice(i, 1);
localStorage.setItem('users', JSON.stringify(users));
break;
}
}
// respond 200 OK
// @ts-ignore
resolve({ ok: true, text: () => Promise.resolve() });
} else {
// return 401 not authorised if token is null or invalid
reject('Unauthorised');
}
return;
}
// pass through any requests not handled above
realFetch(url, opts).then((response) => resolve(response));
}, 500);
});
};
}

2
src/helpers/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './fake-backend';
export * from './auth-header';

View File

@@ -10,6 +10,11 @@ import FlagIcon from 'vue-flag-icon';
Vue.use(FlagIcon);
// setup fake backend
// import { configureFakeBackend } from './helpers';
// configureFakeBackend();
Vue.config.productionTip = false;
new Vue({

View File

@@ -2,10 +2,13 @@ import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import NotFound from './views/NotFound.vue';
import LoginPage from './views/LoginPage.vue';
Vue.use(Router);
export default new Router({
export const router = new Router({
// export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
@@ -14,6 +17,10 @@ export default new Router({
name: 'home',
component: Home,
},
{
path: '/login',
component: LoginPage,
},
{
path: '/about',
name: 'about',
@@ -29,3 +36,16 @@ export default new Router({
},
],
});
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login', '/register'];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('user');
if (authRequired && !loggedIn) {
return next('/login');
}
});
export default router;

1
src/services/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './user.service';

View File

@@ -0,0 +1,113 @@
import { authHeader } from '@/helpers';
export const userService = {
login,
logout,
register,
getAll,
getById,
update,
delete: _delete,
};
// @ts-ignore
function login(username, password) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
};
return fetch(`${process.env.API_URL}/users/authenticate`, requestOptions)
.then(handleResponse)
.then((user) => {
// login successful if there's a jwt token in the response
if (user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
}
return user;
});
}
function logout() {
// remove user from local storage to log user out
localStorage.removeItem('user');
}
// @ts-ignore
function register(user) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user),
};
return fetch(`${process.env.API_URL}/users/register`, requestOptions).then(handleResponse);
}
function getAll() {
const requestOptions = {
method: 'GET',
headers: authHeader(),
};
// @ts-ignore
return fetch(`${config.apiUrl}/users`, requestOptions).then(handleResponse);
}
// @ts-ignore
function getById(id) {
const requestOptions = {
method: 'GET',
headers: authHeader(),
};
// @ts-ignore
return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
}
// @ts-ignore
function update(user) {
const requestOptions = {
method: 'PUT',
headers: { ...authHeader(), 'Content-Type': 'application/json' },
body: JSON.stringify(user),
};
// @ts-ignore
return fetch(`${config.apiUrl}/users/${user.id}`, requestOptions).then(handleResponse);
}
// prefixed function name with underscore because delete is a reserved word in javascript
// @ts-ignore
function _delete(id) {
const requestOptions = {
method: 'DELETE',
headers: authHeader(),
};
// @ts-ignore
return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
}
// @ts-ignore
function handleResponse(response) {
return response.text().then((text: any) => {
const data = text && JSON.parse(text);
if (!response.ok) {
if (response.status === 401) {
// auto logout if 401 response returned from api
logout();
location.reload(true);
}
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
});
}

View File

@@ -1,9 +1,18 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { alert } from './store/alert.module';
import { account } from './store/account.module';
import { users } from './store/users.module';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
alert,
account,
users,
},
state: {
},

View File

@@ -0,0 +1,94 @@
import { userService } from '@/services';
import { router } from '@/router';
// @ts-ignore
const user = JSON.parse(localStorage.getItem('user'));
const state = user
? { status: { loggedIn: true }, user }
: { status: {}, user: null };
const actions = {
// @ts-ignore
login({ dispatch, commit }, { username, password }) {
commit('loginRequest', { username });
userService.login(username, password)
.then(
(loggedInUser) => {
commit('loginSuccess', loggedInUser);
router.push('/');
},
(error) => {
commit('loginFailure', error);
dispatch('alert/error', error, { root: true });
},
);
},
// @ts-ignore
logout({ commit }) {
userService.logout();
commit('logout');
},
// @ts-ignore
register({ dispatch, commit }, userToRegister) {
commit('registerRequest', userToRegister);
userService.register(userToRegister)
.then(
(registeredUser) => {
commit('registerSuccess', registeredUser);
router.push('/login');
setTimeout(() => {
// display success message after route change completes
dispatch('alert/success', 'Registration successful', { root: true });
});
},
(error) => {
commit('registerFailure', error);
dispatch('alert/error', error, { root: true });
},
);
},
};
const mutations = {
// @ts-ignore
loginRequest(mutState, userToLogin) {
mutState.status = { loggingIn: true };
mutState.user = userToLogin;
},
// @ts-ignore
loginSuccess(mutState, userToLogin) {
mutState.status = { loggedIn: true };
mutState.user = userToLogin;
},
// @ts-ignore
loginFailure(mutState) {
mutState.status = {};
mutState.user = null;
},
// @ts-ignore
logout(mutState) {
mutState.status = {};
mutState.user = null;
},
// @ts-ignore
registerRequest(mutState, userToRegister) {
mutState.status = { registering: true };
},
// @ts-ignore
registerSuccess(mutState, userToRegister) {
mutState.status = {};
},
// @ts-ignore
registerFailure(mutState, error) {
mutState.status = {};
},
};
export const account = {
namespaced: true,
state,
actions,
mutations,
};

44
src/store/alert.module.ts Normal file
View File

@@ -0,0 +1,44 @@
const state = {
type: null,
message: null,
};
const actions = {
// @ts-ignore
success({ commit }, message) {
commit('success', message);
},
// @ts-ignore
error({ commit }, message) {
commit('error', message);
},
// @ts-ignore
clear({ commit }, message) {
commit('success', message);
},
};
const mutations = {
// @ts-ignore
success(mutState, message) {
mutState.type = 'alert-success';
mutState.message = message;
},
// @ts-ignore
error(mutState, message) {
mutState.type = 'alert-danger';
mutState.message = message;
},
// @ts-ignore
clear(mutState) {
mutState.type = null;
mutState.message = null;
},
};
export const alert = {
namespaced: true,
state,
actions,
mutations,
};

78
src/store/users.module.ts Normal file
View File

@@ -0,0 +1,78 @@
import { userService } from '@/services';
const state = {
all: {},
};
const actions = {
// @ts-ignore
getAll({ commit }) {
commit('getAllRequest');
userService.getAll()
.then(
(receivedUsers: any) => commit('getAllSuccess', receivedUsers),
(error: any) => commit('getAllFailure', error),
);
},
// @ts-ignore
delete({ commit }, id) {
commit('deleteRequest', id);
userService.delete(id)
.then(
(user: any) => commit('deleteSuccess', id),
(error: any) => commit('deleteSuccess', { id, error: error.toString() }),
);
},
};
const mutations = {
// @ts-ignore
getAllRequest(mutState) {
mutState.all = { loading: true };
},
// @ts-ignore
getAllSuccess(mutState, receivedUsers) {
mutState.all = { items: receivedUsers };
},
// @ts-ignore
getAllFailure(mutState, error) {
mutState.all = { error };
},
// @ts-ignore
deleteRequest(mutState, id) {
// add 'deleting:true' property to user being deleted
mutState.all.items = mutState.all.items.map((user: any) =>
user.id === id
? { ...user, deleting: true }
: user,
);
},
// @ts-ignore
deleteSuccess(mutState, id) {
// remove deleted user from state
mutState.all.items = mutState.all.items.filter((user: any) => user.id !== id);
},
// @ts-ignore
deleteFailure(mutState, { id, error }) {
// remove 'deleting:true' property and add 'deleteError:[error]' property to user
mutState.all.items = mutState.items.map((user: any) => {
if (user.id === id) {
// make copy of user without 'deleting:true' property
const { deleting, ...userCopy } = user;
// return copy of user with 'deleteError:[error]' property
return { ...userCopy, deleteError: error };
}
return user;
});
},
};
export const users = {
namespaced: true,
state,
actions,
mutations,
};

42
src/views/HomePage.vue Normal file
View File

@@ -0,0 +1,42 @@
<template>
<div>
<h1>Hi {{account.user.firstName}}!</h1>
<p>You're logged in with Vue + Vuex & JWT!!</p>
<h3>Users from secure api end point:</h3>
<em v-if="users.loading">Loading users...</em>
<span v-if="users.error" class="text-danger">ERROR: {{users.error}}</span>
<ul v-if="users.items">
<li v-for="user in users.items" :key="user.id">
{{user.firstName + ' ' + user.lastName}}
<span v-if="user.deleting"><em> - Deleting...</em></span>
<span v-else-if="user.deleteError" class="text-danger"> - ERROR: {{user.deleteError}}</span>
<span v-else> - <a @click="deleteUser(user.id)" class="text-danger">Delete</a></span>
</li>
</ul>
<p>
<router-link to="/login">Logout</router-link>
</p>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState({
account: (state) => state.account,
users: (state) => state.users.all,
}),
},
created() {
this.getAllUsers();
},
methods: {
...mapActions('users', {
getAllUsers: 'getAll',
deleteUser: 'delete',
}),
},
};
</script>

72
src/views/LoginPage.vue Normal file
View File

@@ -0,0 +1,72 @@
<template>
<div>
<div class="alert alert-info">
Username: test<br />
Password: test
</div>
<h2>Login</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="username">Username</label>
<input type="text" v-model="username" name="username" class="form-control" :class="{ 'is-invalid': submitted && !username }" />
<div v-show="submitted && !username" class="invalid-feedback">Username is required</div>
</div>
<div class="form-group">
<label htmlFor="password">Password</label>
<input type="password" v-model="password" name="password" class="form-control" :class="{ 'is-invalid': submitted && !password }" />
<div v-show="submitted && !password" class="invalid-feedback">Password is required</div>
</div>
<div class="form-group">
<button class="btn btn-primary" :disabled="loading">Login</button>
<img v-show="loading" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
</div>
<div v-if="error" class="alert alert-danger">{{error}}</div>
</form>
</div>
</template>
<script>
import router from '@/router/';
import { userService } from '@/services';
export default {
data() {
return {
username: '',
password: '',
submitted: false,
loading: false,
returnUrl: '',
error: '',
};
},
created() {
// reset login status
userService.logout();
// get return url from route parameters or default to '/'
this.returnUrl = this.$route.query.returnUrl || '/';
},
methods: {
handleSubmit(e) {
this.submitted = true;
const { username, password } = this;
// stop here if form is invalid
if (!(username && password)) {
return;
}
this.loading = true;
userService.login(username, password)
.then(
(user) => router.push(this.returnUrl),
(error) => {
this.error = error;
this.loading = false;
},
);
},
},
};
</script>

View File

@@ -0,0 +1,64 @@
<template>
<div>
<h2>Register</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" v-model="user.firstName" v-validate="'required'" name="firstName" class="form-control" :class="{ 'is-invalid': submitted && errors.has('firstName') }" />
<div v-if="submitted && errors.has('firstName')" class="invalid-feedback">{{ errors.first('firstName') }}</div>
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" v-model="user.lastName" v-validate="'required'" name="lastName" class="form-control" :class="{ 'is-invalid': submitted && errors.has('lastName') }" />
<div v-if="submitted && errors.has('lastName')" class="invalid-feedback">{{ errors.first('lastName') }}</div>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" v-model="user.username" v-validate="'required'" name="username" class="form-control" :class="{ 'is-invalid': submitted && errors.has('username') }" />
<div v-if="submitted && errors.has('username')" class="invalid-feedback">{{ errors.first('username') }}</div>
</div>
<div class="form-group">
<label htmlFor="password">Password</label>
<input type="password" v-model="user.password" v-validate="{ required: true, min: 6 }" name="password" class="form-control" :class="{ 'is-invalid': submitted && errors.has('password') }" />
<div v-if="submitted && errors.has('password')" class="invalid-feedback">{{ errors.first('password') }}</div>
</div>
<div class="form-group">
<button class="btn btn-primary" :disabled="status.registering">Register</button>
<img v-show="status.registering" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
<router-link to="/login" class="btn btn-link">Cancel</router-link>
</div>
</form>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
data() {
return {
user: {
firstName: '',
lastName: '',
username: '',
password: '',
},
submitted: false,
};
},
computed: {
...mapState('account', ['status']),
},
methods: {
...mapActions('account', ['register']),
handleSubmit(e) {
this.submitted = true;
this.$validator.validate().then((valid) => {
if (valid) {
this.register(this.user);
}
});
},
},
};
</script>