WIP Auth redesign.
This commit is contained in:
parent
6bc094f515
commit
b23d3d36af
10
web/package-lock.json
generated
10
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
1
web/src/models.d.ts
vendored
@ -36,5 +36,4 @@ export interface User {
|
||||
displayName: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
authToken?: string;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
}
|
||||
|
45
web/src/store-modules/auth.ts
Normal file
45
web/src/store-modules/auth.ts
Normal 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;
|
37
web/src/store-modules/measure.ts
Normal file
37
web/src/store-modules/measure.ts
Normal 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);
|
@ -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;
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
}
|
||||
|
3
web/src/views/measures.scss
Normal file
3
web/src/views/measures.scss
Normal file
@ -0,0 +1,3 @@
|
||||
#measures {
|
||||
flex-grow: 1;
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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" }]
|
||||
|
Loading…
x
Reference in New Issue
Block a user