web: Fix login session persistence using localStorage.

This commit is contained in:
Jonathan Bernard 2019-04-10 10:48:26 -05:00
parent d7387d699d
commit 3154d97dd1
4 changed files with 79 additions and 30 deletions

View File

@ -13,12 +13,14 @@
"@fortawesome/free-solid-svg-icons": "^5.7.2", "@fortawesome/free-solid-svg-icons": "^5.7.2",
"@fortawesome/vue-fontawesome": "^0.1.5", "@fortawesome/vue-fontawesome": "^0.1.5",
"@types/js-cookie": "^2.2.1", "@types/js-cookie": "^2.2.1",
"@types/jwt-decode": "^2.2.1",
"@types/lodash.assign": "^4.2.6", "@types/lodash.assign": "^4.2.6",
"@types/lodash.findindex": "^4.6.6", "@types/lodash.findindex": "^4.6.6",
"@types/lodash.merge": "^4.6.5", "@types/lodash.merge": "^4.6.5",
"apexcharts": "^3.6.5", "apexcharts": "^3.6.5",
"axios": "^0.18.0", "axios": "^0.18.0",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"jwt-decode": "^2.2.0",
"keen-ui": "^1.1.2", "keen-ui": "^1.1.2",
"lodash.assign": "^4.2.0", "lodash.assign": "^4.2.0",
"lodash.findindex": "^4.6.0", "lodash.findindex": "^4.6.0",

View File

@ -6,9 +6,12 @@ import Measures from '@/views/Measures.vue';
import UserAccount from '@/views/UserAccount.vue'; import UserAccount from '@/views/UserAccount.vue';
import QuickPanels from '@/views/QuickPanels.vue'; import QuickPanels from '@/views/QuickPanels.vue';
import { authStore } from '@/store'; import { authStore } from '@/store';
import { logService } from '@/services/logging';
Vue.use(Router); Vue.use(Router);
const logger = logService.getLogger('/router');
const router = new Router({ const router = new Router({
mode: 'history', mode: 'history',
base: process.env.BASE_URL, base: process.env.BASE_URL,
@ -20,7 +23,8 @@ const router = new Router({
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: Login component: Login,
props: (route) => ({ redirect: route.query.redirect })
}, },
{ {
path: '/measures', path: '/measures',
@ -43,12 +47,26 @@ const router = new Router({
] ]
}); });
async function getAuthToken(): Promise<string | null> {
if (authStore.authToken) { return authStore.authToken; }
logger.trace('Checking for an existing API token.');
try {
const token = await authStore.findLocalToken();
return token;
}
catch (e) {
logger.info('Could not find a valid token.', e);
return null;
}
}
// Auth filter // Auth filter
router.beforeEach((to, from, next) => { router.beforeEach(async (to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) { if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!authStore.authToken) { const token = await getAuthToken();
next({ name: 'login' }); if (!token) {
// params: { redirect: to.path } }); next({ name: 'login', query: { redirect: to.path } });
} else { next(); } // if authed } else { next(); } // if authed
} else { next(); } // if auth required } else { next(); } // if auth required
}); });

View File

@ -4,38 +4,72 @@ import {
Mutation, Mutation,
VuexModule VuexModule
} from 'vuex-module-decorators'; } from 'vuex-module-decorators';
import Cookies from 'js-cookie'; import JwtDecode from 'jwt-decode';
import { LoginSubmit } from '@/models'; import { LoginSubmit } from '@/models';
import api from '@/services/pm-api-client'; import api from '@/services/pm-api-client';
import { logService } from '@/services/logging'; import { logService } from '@/services/logging';
import { userStore, measureStore } from '@/store'; import { userStore, measureStore } from '@/store';
const COOKIE_TOKEN = 'pm-api-cookie-token'; const SESSION_KEY = 'session-token';
const log = logService.getLogger('/store-modules/auth'); const logger = logService.getLogger('/store-modules/auth');
export interface SessionToken {
iat: string;
exp: number;
sub: string;
}
@Module({ namespaced: true, name: 'auth' }) @Module({ namespaced: true, name: 'auth' })
export class AuthStoreModule extends VuexModule { export class AuthStoreModule extends VuexModule {
public authToken: string | null = null; public authToken: string | null = null;
@Action({ commit: 'SET_TOKEN', rawError: true }) @Action({ rawError: true })
public async login(creds: LoginSubmit) { public async login(creds: LoginSubmit): Promise<string> {
const authToken = await api.createAuthToken(creds); // API will cache this token const authToken = await api.createAuthToken(creds); // API will cache this token
Cookies.set(COOKIE_TOKEN, authToken, { secure: true });
await userStore.fetchUser(); // this should be guaranteed by the server (redirect HTTP -> HTTPS)
await measureStore.fetchAllMeasures(); // but we'll do a sanity check just to make sure.
log.trace('User login successful.'); if (window.location.protocol === 'https:') {
localStorage.setItem(SESSION_KEY, authToken);
}
logger.trace('User login successful.');
this.context.commit('SET_TOKEN', authToken);
this.loadInitialState();
return authToken; return authToken;
} }
@Action({ commit: 'SET_TOKEN', rawError: true }) @Action({ rawError: true })
public async findLocalToken() { public async findLocalToken(): Promise<string> {
const token = Cookies.get(COOKIE_TOKEN); const encodedToken = localStorage.getItem(SESSION_KEY);
if (token) { return token; } if (!encodedToken) {
else { throw new Error('No auth token stored as a cookie.'); } logger.trace('encodedToken was falsey');
throw new Error('Could not find an existing auth token.');
}
const token = JwtDecode<SessionToken>(encodedToken);
if ((new Date(token.exp * 1000)) < new Date()) {
logger.trace('token has expired: token.exp = ' + (token.exp * 1000));
throw new Error ('The existing auth token has expired.');
}
api.setAuthToken(encodedToken);
this.context.commit('SET_TOKEN', encodedToken);
this.loadInitialState();
return encodedToken;
}
@Action
public async loadInitialState() {
userStore.fetchUser();
measureStore.fetchAllMeasures();
} }
@Mutation private SET_TOKEN(t: string) { @Mutation private SET_TOKEN(t: string) {
logger.info('SET_TOKEN called');
this.authToken = t; this.authToken = t;
} }
} }

View File

@ -4,10 +4,10 @@ import { LoginSubmit } from '@/models';
import { authStore } from '@/store'; import { authStore } from '@/store';
import { logService } from '@/services/logging'; import { logService } from '@/services/logging';
const log = logService.getLogger('/views/login'); const logger = logService.getLogger('/views/login');
@Component({}) @Component({})
export default class Login extends Vue { export class Login extends Vue {
private loginForm: LoginSubmit = { private loginForm: LoginSubmit = {
email: '', email: '',
@ -16,7 +16,7 @@ export default class Login extends Vue {
private waiting = false; private waiting = false;
private flashMessage = ''; private flashMessage = '';
private redirect: string | undefined = undefined; @Prop() private redirect!: string | null;
public async login() { public async login() {
this.waiting = true; this.waiting = true;
@ -29,18 +29,13 @@ export default class Login extends Vue {
this.flashMessage = 'invlid username or password'; this.flashMessage = 'invlid username or password';
} else { } else {
this.flashMessage = 'unable to log you in'; this.flashMessage = 'unable to log you in';
log.error(e); logger.error(e);
} }
} }
this.waiting = false; this.waiting = false;
} }
/*
@Watch('$route', { immediate: true })
private onRouteChange(route: Route) {
this.redirect = route.query && route.query.redirect as string;
}
*/
} }
// TODO: styling of flash message // TODO: styling of flash message
export default Login;