web: Fix login session persistence using localStorage.
This commit is contained in:
parent
d7387d699d
commit
3154d97dd1
@ -13,12 +13,14 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^5.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.5",
|
||||
"@types/js-cookie": "^2.2.1",
|
||||
"@types/jwt-decode": "^2.2.1",
|
||||
"@types/lodash.assign": "^4.2.6",
|
||||
"@types/lodash.findindex": "^4.6.6",
|
||||
"@types/lodash.merge": "^4.6.5",
|
||||
"apexcharts": "^3.6.5",
|
||||
"axios": "^0.18.0",
|
||||
"js-cookie": "^2.2.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"keen-ui": "^1.1.2",
|
||||
"lodash.assign": "^4.2.0",
|
||||
"lodash.findindex": "^4.6.0",
|
||||
|
@ -6,9 +6,12 @@ import Measures from '@/views/Measures.vue';
|
||||
import UserAccount from '@/views/UserAccount.vue';
|
||||
import QuickPanels from '@/views/QuickPanels.vue';
|
||||
import { authStore } from '@/store';
|
||||
import { logService } from '@/services/logging';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
const logger = logService.getLogger('/router');
|
||||
|
||||
const router = new Router({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
@ -20,7 +23,8 @@ const router = new Router({
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login
|
||||
component: Login,
|
||||
props: (route) => ({ redirect: route.query.redirect })
|
||||
},
|
||||
{
|
||||
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
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||
if (!authStore.authToken) {
|
||||
next({ name: 'login' });
|
||||
// params: { redirect: to.path } });
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
next({ name: 'login', query: { redirect: to.path } });
|
||||
} else { next(); } // if authed
|
||||
} else { next(); } // if auth required
|
||||
});
|
||||
|
@ -4,38 +4,72 @@ import {
|
||||
Mutation,
|
||||
VuexModule
|
||||
} from 'vuex-module-decorators';
|
||||
import Cookies from 'js-cookie';
|
||||
import JwtDecode from 'jwt-decode';
|
||||
import { LoginSubmit } from '@/models';
|
||||
import api from '@/services/pm-api-client';
|
||||
import { logService } from '@/services/logging';
|
||||
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' })
|
||||
export class AuthStoreModule extends VuexModule {
|
||||
public authToken: string | null = null;
|
||||
|
||||
@Action({ commit: 'SET_TOKEN', rawError: true })
|
||||
public async login(creds: LoginSubmit) {
|
||||
@Action({ rawError: true })
|
||||
public async login(creds: LoginSubmit): Promise<string> {
|
||||
const authToken = await api.createAuthToken(creds); // API will cache this token
|
||||
Cookies.set(COOKIE_TOKEN, authToken, { secure: true });
|
||||
await userStore.fetchUser();
|
||||
await measureStore.fetchAllMeasures();
|
||||
log.trace('User login successful.');
|
||||
|
||||
// this should be guaranteed by the server (redirect HTTP -> HTTPS)
|
||||
// but we'll do a sanity check just to make sure.
|
||||
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;
|
||||
}
|
||||
|
||||
@Action({ commit: 'SET_TOKEN', rawError: true })
|
||||
public async findLocalToken() {
|
||||
const token = Cookies.get(COOKIE_TOKEN);
|
||||
if (token) { return token; }
|
||||
else { throw new Error('No auth token stored as a cookie.'); }
|
||||
@Action({ rawError: true })
|
||||
public async findLocalToken(): Promise<string> {
|
||||
const encodedToken = localStorage.getItem(SESSION_KEY);
|
||||
if (!encodedToken) {
|
||||
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) {
|
||||
logger.info('SET_TOKEN called');
|
||||
this.authToken = t;
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ import { LoginSubmit } from '@/models';
|
||||
import { authStore } from '@/store';
|
||||
import { logService } from '@/services/logging';
|
||||
|
||||
const log = logService.getLogger('/views/login');
|
||||
const logger = logService.getLogger('/views/login');
|
||||
|
||||
@Component({})
|
||||
export default class Login extends Vue {
|
||||
export class Login extends Vue {
|
||||
|
||||
private loginForm: LoginSubmit = {
|
||||
email: '',
|
||||
@ -16,7 +16,7 @@ export default class Login extends Vue {
|
||||
|
||||
private waiting = false;
|
||||
private flashMessage = '';
|
||||
private redirect: string | undefined = undefined;
|
||||
@Prop() private redirect!: string | null;
|
||||
|
||||
public async login() {
|
||||
this.waiting = true;
|
||||
@ -29,18 +29,13 @@ export default class Login extends Vue {
|
||||
this.flashMessage = 'invlid username or password';
|
||||
} else {
|
||||
this.flashMessage = 'unable to log you in';
|
||||
log.error(e);
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
export default Login;
|
||||
|
Loading…
x
Reference in New Issue
Block a user