WIP Auth redesign.

This commit is contained in:
Jonathan Bernard 2019-03-07 23:39:24 -06:00
parent 6bc094f515
commit b23d3d36af
18 changed files with 188 additions and 51 deletions

10
web/package-lock.json generated
View File

@ -1004,6 +1004,11 @@
"integrity": "sha512-Q5hTcfdudEL2yOmluA1zaSyPbzWPmJ3XfSWeP3RyoYvS9hnje1ZyagrZOuQ6+1nQC1Gw+7gap3pLNL3xL6UBug==",
"dev": true
},
"@types/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-VIVurImEhQ95jxtjs8baVU5qCzVfwYfuMrpXwdRykJ5MCI5iY7/jB4cDSgwBVeYqeXrhT7GfJUwoDOmN0OMVCA=="
},
"@types/lodash": {
"version": "4.14.121",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.121.tgz",
@ -9145,6 +9150,11 @@
"nopt": "~4.0.1"
}
},
"js-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz",
"integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s="
},
"js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",

View File

@ -12,8 +12,10 @@
"@fortawesome/fontawesome-svg-core": "^1.2.15",
"@fortawesome/free-solid-svg-icons": "^5.7.2",
"@fortawesome/vue-fontawesome": "^0.1.5",
"@types/js-cookie": "^2.2.1",
"@types/lodash.merge": "^4.6.5",
"axios": "^0.18.0",
"js-cookie": "^2.2.0",
"keen-ui": "^1.1.2",
"lodash.merge": "^4.6.1",
"register-service-worker": "^1.5.2",

View File

@ -1,10 +1,11 @@
import { Component, Vue, Watch } from 'vue-property-decorator';
import NavBar from '@/views/NavBar.vue';
import { logService, LogLevel, ApiLogAppender, ConsoleLogAppender } from '@/services/logging';
import usersStore from '@/store-modules/user';
import authStore from '@/store-modules/auth';
import measureStore from '@/store-modules/measure';
import userStore from '@/store-modules/user';
import { User } from '@/models';
@Component({
components: {
NavBar
@ -19,24 +20,30 @@ export default class App extends Vue {
super();
// Setup application logging.
// TODO: prod/dev config settings for logging?
this.consoleLogAppender = new ConsoleLogAppender(LogLevel.ALL);
this.apiLogAppender = new ApiLogAppender(
process.env.VUE_APP_PM_API_BASE + '/log/batch', '', LogLevel.WARN);
logService.ROOT_LOGGER.appenders.push(this.apiLogAppender, this.consoleLogAppender);
// TODO: prod/dev config settings for logging?
this.apiLogAppender.batchSize = 1;
this.apiLogAppender.minimumTimePassedInSec = 5;
logService.ROOT_LOGGER.appenders.push(this.apiLogAppender, this.consoleLogAppender);
// Check for existing session cookie.
/*tslint:disable:no-empty*/
authStore.findLocalToken().catch(() => {});
}
private get user(): User | null {
return usersStore.user;
private get authToken(): string | undefined {
return authStore.authToken;
}
@Watch('user')
private onUserChanged(val: User | null , oldVal: User | null) {
if (val) { this.apiLogAppender.authToken = val.authToken; }
@Watch('authToken')
private onAuthTokenChange(val: string | undefined , oldVal: string | undefined) {
if (val) {
this.apiLogAppender.authToken = val;
userStore.fetchUser();
measureStore.fetchAllMeasures();
}
}
}

1
web/src/models.d.ts vendored
View File

@ -36,5 +36,4 @@ export interface User {
displayName: string;
email: string;
isAdmin: boolean;
authToken?: string;
}

View File

@ -6,7 +6,7 @@ import Login from '@/views/Login.vue';
import Measures from '@/views/Measures.vue';
import UserAccount from '@/views/UserAccount.vue';
import QuickPanels from '@/views/QuickPanels.vue';
import userStore from '@/store-modules/user';
import authStore from '@/store-modules/auth';
Vue.use(Router);
@ -16,7 +16,7 @@ const router = new Router({
routes: [
{
path: '/',
redirect: '/dashboard'
redirect: '/measures'
},
{
path: '/login',
@ -53,7 +53,7 @@ const router = new Router({
// Auth filter
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!userStore.user || !userStore.user.authToken) {
if (!authStore.authToken) {
next({ name: 'login' });
// params: { redirect: to.path } });
} else { next(); } // if authed

View File

@ -2,10 +2,10 @@ import { default as Axios, AxiosInstance } from 'axios';
import { ApiToken, LoginSubmit, Measure, Measurement, User } from '@/models';
import { Logger, logService } from '@/services/logging';
import merge from 'lodash.merge';
import authStore from '@/store-modules/auth';
export class PmApiClient {
private http: AxiosInstance;
private authToken?: string;
private log: Logger;
constructor(apiBase: string) {
@ -24,8 +24,6 @@ export class PmApiClient {
this.log.trace('Initialized PmApiClient');
}
public setAuthToken(t: string) { this.authToken = t; }
public async version(): Promise<string> {
const resp = await this.http.get('/version');
return resp.data;
@ -38,12 +36,12 @@ export class PmApiClient {
return resp.data;
}
public async getUser(authToken: string): Promise<User> {
public async getUser(): Promise<User> {
const resp = await this.http.get('/user',
{ headers: { Authorization: 'Bearer ' + authToken }});
{ headers: this.authHeader() });
return merge(resp.data, { authToken }) ;
return merge(resp.data) ;
}
public async getAllUsers(): Promise<User[]> {
@ -206,8 +204,8 @@ export class PmApiClient {
}
private authHeader(): { [key: string]: string } {
if (this.authToken) {
return { Authorization: 'Bearer ' + this.authToken };
if (authStore.authToken) {
return { Authorization: 'Bearer ' + authStore.authToken };
} else {
throw new Error('no authenticated user');
}

View File

@ -0,0 +1,45 @@
import {
Action,
getModule,
Module,
Mutation,
MutationAction,
VuexModule
} from 'vuex-module-decorators';
import { LoginSubmit } from '@/models';
import store from '@/store';
import api from '@/services/pm-api-client';
import Cookies from 'js-cookie';
const COOKIE_TOKEN = 'pm-api-cookie-token';
@Module({ namespaced: true, name: 'auth', store, dynamic: true })
class AuthStoreModule extends VuexModule {
public authToken: string | undefined = undefined;
@Action({ commit: 'SET_TOKEN', rawError: true })
public async login(creds: LoginSubmit) {
return await api.createAuthToken(creds);
}
@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({ commit: 'SET_TOKEN' })
public setToken(t: string) {
// TODO: set the expires based on the JWT token expiry?
Cookies.set(COOKIE_TOKEN, t, { secure: true });
return t;
}
@Mutation public SET_TOKEN(t: string) {
this.authToken = t;
}
}
export const authStore = getModule(AuthStoreModule);
export default authStore;

View File

@ -0,0 +1,37 @@
import {
Action,
getModule,
Module,
Mutation,
MutationAction,
VuexModule
} from 'vuex-module-decorators';
import { keyBy } from 'lodash';
import { User, Measure } from '@/models';
import store from '@/store';
import api from '@/services/pm-api-client';
import { logService } from '@/services/logging';
@Module({ namespaced: true, name: 'measure', store, dynamic: true })
class MeasureStoreModule extends VuexModule {
public measures: { [key: string]: Measure } = {};
private log = logService.getLogger('/store-modules/measure');
@MutationAction({ mutate: ['measures'], rawError: true })
public async fetchAllMeasures() {
const measures = await api.getAllMeasures();
return { measures: keyBy(measures, 'slug') };
}
@Action({ commit: 'SET_MEASURE', rawError: true })
public async fetchMeasure(slug: string) {
return await api.getMeasure(slug);
}
@Mutation private SET_MEASURE(measure: Measure) {
this.measures[measure.slug] = measure;
}
}
export default getModule(MeasureStoreModule);

View File

@ -7,8 +7,8 @@ import {
VuexModule
} from 'vuex-module-decorators';
import { LoginSubmit, User } from '@/models';
import store from '@/store';
import api from '@/services/pm-api-client';
import store from '@/store';
import { logService } from '@/services/logging';
@Module({ namespaced: true, name: 'user', store, dynamic: true })
@ -18,20 +18,14 @@ class UserStoreModule extends VuexModule {
private log = logService.getLogger('/store-modules/user');
@Action({ commit: 'SET_USER', rawError: true })
public async login(creds: LoginSubmit) {
const authToken = await api.createAuthToken(creds);
const user = await api.getUser(authToken);
this.log.trace('User login successful.');
api.setAuthToken(authToken);
return user;
@MutationAction({ mutate: ['user']})
public async fetchUser() {
return { user: await api.getUser() };
}
@MutationAction({ mutate: ['users'] })
public async fetchAllUsers() {
const users = await api.getAllUsers();
return { users };
return { users: await api.getAllUsers() };
}
@MutationAction({ mutate: ['users'] })
@ -45,8 +39,7 @@ class UserStoreModule extends VuexModule {
await api.deleteUser(userId);
return { users: this.users.filter((u) => u.id !== userId) };
}
@Mutation private SET_USER(user: User) { this.user = user; }
}
export default getModule(UserStoreModule);
export const userStore = getModule(UserStoreModule);
export default userStore;

View File

@ -15,3 +15,13 @@
background-color: darken($color2, 5%);
}
}
.header-action {
align-items: baseline;
display: flex;
font-size: 1.2rem;
justify-content: space-between;
& > * { display: inline-block; }
& > button { font-size: inherit; }
}

View File

@ -1,6 +1,13 @@
<template>
<div class="measures">
<h1>Things You Are Measuring</h1>
<div id="measures">
<div class=header-action>
<h1>Things You Are Measuring</h1>
<button class=btn-action>Add Measure</button>
</div>
<div v-for="(measure, slug) in measures">
<h2>{{measure.name}}</h2>
</div>
</div>
</template>
<script lang="ts" src="./measures.ts"></script>
<style lang="scss" src="./measures.scss"></style>

View File

@ -4,13 +4,19 @@
<span class=expanded>Personal Measure</span>
<span class=collapsed>PM</span>
</h1>
<router-link to="/dashboard">
<!--<router-link to="/dashboard">
<fa-icon icon=home></fa-icon>
<span class=expanded>Dashboard</span>
</router-link>
</router-link>-->
<router-link to="/measures">
<fa-icon icon=pencil-ruler></fa-icon>
<span class=expanded>Measures</span>
<div class="submenu expanded">
<router-link v-for="(measure, slug) in measures" :key="slug"
:to="'/measures/' + slug">
{{measure.name}}
</router-link>
</div>
</router-link>
<router-link to="/quick-panels">
<fa-icon icon=th-large></fa-icon>

View File

@ -1,7 +1,7 @@
import { Route } from 'vue-router';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { LoginSubmit } from '@/models';
import userStore from '@/store-modules/user';
import authStore from '@/store-modules/auth';
@Component({})
export default class Login extends Vue {
@ -19,9 +19,10 @@ export default class Login extends Vue {
this.waiting = true;
this.flashMessage = '';
try {
await userStore.login(this.loginForm);
await authStore.login(this.loginForm);
this.$router.push({ path: this.redirect || '/' });
} catch (e) {
console.log(e);
if (e.response.status === 401) {
this.flashMessage = 'invlid username or password';
}

View File

@ -0,0 +1,3 @@
#measures {
flex-grow: 1;
}

View File

@ -1,6 +1,9 @@
import { Component, Vue } from 'vue-property-decorator';
import measureStore from '@/store-modules/measure';
@Component({
components: { }
})
export default class Measures extends Vue {}
export default class Measures extends Vue {
private get measures() { return measureStore.measures; }
}

View File

@ -21,14 +21,16 @@ nav {
a {
color: inherit;
display: block;
font-size: 1.5rem;
padding: .5rem 0;
text-decoration: none;
width: 100%;
&.router-link-active { color: $color4; }
&:hover { color: $color4; }
}
& > a {
font-size: 1.5rem;
padding: .5rem 0;
svg {
display: inline-block;
@ -40,9 +42,21 @@ nav {
padding: 0 0 1rem;
}
.collapse-handle {
& > .collapse-handle {
cursor: pointer;
font-size: 1.5rem;
margin-top: auto;
}
.submenu {
margin: .5em 0 0 1.6em;
a {
color: $color3;
font-size: 1.2rem;
&.router-link-active { color: $color4; }
&:hover { color: $color4; }
}
}
}

View File

@ -1,8 +1,10 @@
import { Component, Vue } from 'vue-property-decorator';
import { Route, RawLocation } from 'vue-router';
import { library } from '@fortawesome/fontawesome-svg-core';
import { faAngleDoubleLeft, faAngleDoubleRight,
faHome, faPencilRuler, faThLarge, faUser } from '@fortawesome/free-solid-svg-icons';
import userStore from '@/store-modules/user';
import measureStore from '@/store-modules/measure';
// import UiIconButton from 'keen-ui/src/UiIconButton.vue';
library.add(faAngleDoubleLeft, faAngleDoubleRight, faHome, faPencilRuler, faThLarge, faUser);
@ -25,7 +27,6 @@ export default class NavBar extends Vue {
return this.collapsed;
}
public get user() {
return userStore.user;
}
public get user() { return userStore.user; }
public get measures() { return measureStore.measures; }
}

View File

@ -13,6 +13,7 @@
"indent": [true, "spaces", 2],
"interface-name": false,
"ordered-imports": false,
"one-line": false,
"object-literal-sort-keys": false,
"no-consecutive-blank-lines": false,
"trailing-comma": [true, {"multiline": "never", "singleline": "never" }]