added (not yet working) code for login
This commit is contained in:
@@ -5,6 +5,10 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<link href="//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
a { cursor: pointer; }
|
||||||
|
</style>
|
||||||
<title>vue_ts_test</title>
|
<title>vue_ts_test</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
11
src/helpers/auth-header.ts
Normal file
11
src/helpers/auth-header.ts
Normal 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
148
src/helpers/fake-backend.ts
Normal 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
2
src/helpers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './fake-backend';
|
||||||
|
export * from './auth-header';
|
||||||
@@ -10,6 +10,11 @@ import FlagIcon from 'vue-flag-icon';
|
|||||||
|
|
||||||
Vue.use(FlagIcon);
|
Vue.use(FlagIcon);
|
||||||
|
|
||||||
|
// setup fake backend
|
||||||
|
// import { configureFakeBackend } from './helpers';
|
||||||
|
// configureFakeBackend();
|
||||||
|
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import Vue from 'vue';
|
|||||||
import Router from 'vue-router';
|
import Router from 'vue-router';
|
||||||
import Home from './views/Home.vue';
|
import Home from './views/Home.vue';
|
||||||
import NotFound from './views/NotFound.vue';
|
import NotFound from './views/NotFound.vue';
|
||||||
|
import LoginPage from './views/LoginPage.vue';
|
||||||
|
|
||||||
|
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
export default new Router({
|
export const router = new Router({
|
||||||
|
// export default new Router({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
base: process.env.BASE_URL,
|
base: process.env.BASE_URL,
|
||||||
routes: [
|
routes: [
|
||||||
@@ -14,6 +17,10 @@ export default new Router({
|
|||||||
name: 'home',
|
name: 'home',
|
||||||
component: Home,
|
component: Home,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
component: LoginPage,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: '/about',
|
||||||
name: '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
1
src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './user.service';
|
||||||
113
src/services/user.service.ts
Normal file
113
src/services/user.service.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
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);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
alert,
|
||||||
|
account,
|
||||||
|
users,
|
||||||
|
},
|
||||||
state: {
|
state: {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
94
src/store/account.module.ts
Normal file
94
src/store/account.module.ts
Normal 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
44
src/store/alert.module.ts
Normal 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
78
src/store/users.module.ts
Normal 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
42
src/views/HomePage.vue
Normal 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
72
src/views/LoginPage.vue
Normal 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>
|
||||||
64
src/views/RegisterPage.vue
Normal file
64
src/views/RegisterPage.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user