WIP UI implementation: fixed store dependencies, login flow.

This commit is contained in:
Jonathan Bernard 2019-03-08 03:03:47 -06:00
parent 9a9fa7c5d9
commit 12b2e5edca
17 changed files with 145 additions and 161 deletions

View File

@ -1,9 +1,7 @@
import { Component, Vue, Watch } from 'vue-property-decorator';
import NavBar from '@/views/NavBar.vue';
import { logService, LogLevel, ApiLogAppender, ConsoleLogAppender } from '@/services/logging';
import authStore from '@/store-modules/auth';
import measureStore from '@/store-modules/measure';
import userStore from '@/store-modules/user';
import { authStore } from '@/store';
import { User } from '@/models';
@Component({
@ -34,16 +32,12 @@ export default class App extends Vue {
authStore.findLocalToken().catch(() => {});
}
private get authToken(): string | undefined {
private get authToken(): string | null {
return authStore.authToken;
}
@Watch('authToken')
private onAuthTokenChange(val: string | undefined , oldVal: string | undefined) {
if (val) {
this.apiLogAppender.authToken = val;
userStore.fetchUser();
measureStore.fetchAllMeasures();
}
if (val) { this.apiLogAppender.authToken = val; }
}
}

View File

@ -2,8 +2,8 @@ import './class-component-hooks';
import Vue from 'vue';
import App from './App.vue';
import { store } from './store';
import router from './router';
import store from './store';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import './registerServiceWorker';

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

@ -3,6 +3,8 @@ export interface ApiToken {
userId: string;
name: string;
value?: string;
created: Date;
expires?: Date;
}
export interface LoginSubmit {

View File

@ -1,12 +1,11 @@
import Vue from 'vue';
import { default as Router, Route } from 'vue-router';
import Dashboard from '@/views/Dashboard.vue';
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 authStore from '@/store-modules/auth';
import { authStore } from '@/store';
Vue.use(Router);
@ -23,12 +22,6 @@ const router = new Router({
name: 'login',
component: Login
},
{
path: '/dashboard',
name: 'dashboard',
component: Dashboard,
meta: { requiresAuth: true }
},
{
path: '/measures',
name: 'measures',

View File

@ -2,7 +2,6 @@ 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;
@ -24,6 +23,15 @@ export class PmApiClient {
this.log.trace('Initialized PmApiClient');
}
public setAuthToken(authToken: string) {
/*tslint:disable:no-string-literal*/
this.http.defaults.headers.common['Authorization'] = `Bearer ${authToken}`;
}
public clearAuthToken() {
this.http.defaults.headers.common['Authorization'] = '';
}
public async version(): Promise<string> {
const resp = await this.http.get('/version');
return resp.data;
@ -33,129 +41,97 @@ export class PmApiClient {
: Promise<string> {
const resp = await this.http.post('/auth-token', creds);
this.setAuthToken(resp.data);
return resp.data;
}
public async getUser(): Promise<User> {
const resp = await this.http.get('/user',
{ headers: this.authHeader() });
return merge(resp.data) ;
const resp = await this.http.get('/user');
return resp.data;
}
public async getAllUsers(): Promise<User[]> {
const resp = await this.http.get('/users',
{ headers: this.authHeader() });
const resp = await this.http.get('/users');
return resp.data;
}
public async getUserById(reqUserId: string): Promise<User> {
const resp = await this.http.get(`/users/${reqUserId}`,
{ headers: this.authHeader() });
const resp = await this.http.get(`/users/${reqUserId}`);
return resp.data;
}
public async createUser(newUser: User): Promise<User> {
const resp = await this.http.post('/users',
newUser, { headers: this.authHeader() });
const resp = await this.http.post('/users', newUser);
return resp.data;
}
public async deleteUser(toDeleteUserId: string)
: Promise<boolean> {
await this.http.delete(`/users/${toDeleteUserId}`,
{ headers: this.authHeader() });
await this.http.delete(`/users/${toDeleteUserId}`);
return true;
}
public async changePwd(oldPassword: string, newPassword: string)
: Promise<boolean> {
await this.http.post('/change-pwd',
{ oldPassword, newPassword }, { headers: this.authHeader() });
await this.http.post('/change-pwd', { oldPassword, newPassword });
return true;
}
public async changePwdForUser(forUserId: string, newPassword: string)
: Promise<boolean> {
await this.http.post(`/change-pwd/${forUserId}`,
{ newPassword }, { headers: this.authHeader() });
await this.http.post(`/change-pwd/${forUserId}`, { newPassword });
return true;
}
public async createApiToken(token: ApiToken)
: Promise<ApiToken[]> {
const resp = await this.http.post(`/api-tokens`,
token, { headers: this.authHeader() });
const resp = await this.http.post(`/api-tokens`, token);
return resp.data;
}
public async getAllApiTokens(): Promise<ApiToken[]> {
const resp = await this.http.get('/api-tokens',
{ headers: this.authHeader() });
const resp = await this.http.get('/api-tokens');
return resp.data;
}
public async getApiToken(tokenId: string): Promise<ApiToken[]> {
const resp = await this.http.get(`/api-tokens/${tokenId}`,
{ headers: this.authHeader() });
const resp = await this.http.get(`/api-tokens/${tokenId}`);
return resp.data;
}
public async deleteApiToken(tokenId: string): Promise<boolean> {
const resp = await this.http.delete(`/api-tokens/${tokenId}`,
{ headers: this.authHeader() });
const resp = await this.http.delete(`/api-tokens/${tokenId}`);
return true;
}
public async getAllMeasures(): Promise<Measure[]> {
const resp = await this.http.get(`/measures`,
{ headers: this.authHeader() });
const resp = await this.http.get(`/measures`);
return resp.data;
}
public async createMeasure(measure: Measure): Promise<Measure> {
const resp = await this.http.post(`/measures`,
measure, { headers: this.authHeader() });
const resp = await this.http.post(`/measures`);
return resp.data;
}
public async getMeasure(slug: string): Promise<Measure> {
const resp = await this.http.get(`/measures/${slug}`,
{ headers: this.authHeader() });
const resp = await this.http.get(`/measures/${slug}`);
return resp.data;
}
public async deleteMeasure(slug: string): Promise<boolean> {
const resp = await this.http.delete(`/measures/${slug}`,
{ headers: this.authHeader() });
const resp = await this.http.delete(`/measures/${slug}`);
return true;
}
public async getMeasurements(measureSlug: string)
: Promise<Measurement[]> {
const resp = await this.http.get(`/measure/${measureSlug}`,
{ headers: this.authHeader() });
const resp = await this.http.get(`/measure/${measureSlug}`);
return resp.data;
}
@ -164,9 +140,7 @@ export class PmApiClient {
measurement: Measurement)
: Promise<Measurement> {
const resp = await this.http.post(`/measure/${measureSlug}`,
measurement, { headers: this.authHeader() });
const resp = await this.http.post(`/measure/${measureSlug}`, measurement);
return resp.data;
}
@ -175,9 +149,8 @@ export class PmApiClient {
measurementId: string)
: Promise<Measurement> {
const resp = await this.http.get(`/measure/${measureSlug}/${measurementId}`,
{ headers: this.authHeader() });
const resp = await this.http
.get(`/measure/${measureSlug}/${measurementId}`);
return resp.data;
}
@ -186,9 +159,8 @@ export class PmApiClient {
measurement: Measurement)
: Promise<Measurement> {
const resp = await this.http.put(`/measure/${measureSlug}/${measurement.id}`,
measurement, { headers: this.authHeader() });
const resp = await this.http
.put(`/measure/${measureSlug}/${measurement.id}`, measurement);
return resp.data;
}
@ -197,19 +169,10 @@ export class PmApiClient {
measurementId: string)
: Promise<boolean> {
const resp = await this.http.delete(`/measure/${measureSlug}/${measurementId}`,
{headers: this.authHeader() });
const resp = await this.http
.delete(`/measure/${measureSlug}/${measurementId}`);
return true;
}
private authHeader(): { [key: string]: string } {
if (authStore.authToken) {
return { Authorization: 'Bearer ' + authStore.authToken };
} else {
throw new Error('no authenticated user');
}
}
}
export const api = new PmApiClient(process.env.VUE_APP_PM_API_BASE);

View File

@ -0,0 +1,33 @@
import {
Action,
Module,
Mutation,
MutationAction,
VuexModule
} from 'vuex-module-decorators';
import api from '@/services/pm-api-client';
import { logService } from '@/services/logging';
import { ApiToken } from '@/models';
const log = logService.getLogger('/store-modules/api-tokens');
@Module({ namespaced: true, name: 'apiToken' })
export class ApiTokenStoreModule extends VuexModule {
public apiTokens: ApiToken[] = [];
@MutationAction({ mutate: ['apiTokens'] })
public async fetchAllApiTokens() {
return { apiTokens: await api.getAllApiTokens() };
}
@MutationAction({ mutate: ['apiTokens'] })
public async createApiToken(newToken: ApiToken) {
return { apiTokens: await api.createApiToken(newToken) };
}
@MutationAction({ mutate: ['apiTokens'] })
public async deleteApiToken(tokenId: string) {
api.deleteApiToken(tokenId);
return { apiTokens: this.apiTokens.filter((tok) => tok.id !== tokenId) };
}
}

View File

@ -1,25 +1,31 @@
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';
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';
@Module({ namespaced: true, name: 'auth', store, dynamic: true })
class AuthStoreModule extends VuexModule {
public authToken: string | undefined = undefined;
const log = logService.getLogger('/store-modules/auth');
@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) {
return await api.createAuthToken(creds);
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.');
return authToken;
}
@Action({ commit: 'SET_TOKEN', rawError: true })
@ -29,17 +35,7 @@ class AuthStoreModule extends VuexModule {
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) {
@Mutation private SET_TOKEN(t: string) {
this.authToken = t;
}
}
export const authStore = getModule(AuthStoreModule);
export default authStore;

View File

@ -8,12 +8,11 @@ import {
} 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 {
@Module({ namespaced: true, name: 'measure' })
export class MeasureStoreModule extends VuexModule {
public measures: { [key: string]: Measure } = {};
private log = logService.getLogger('/store-modules/measure');
@ -33,5 +32,3 @@ class MeasureStoreModule extends VuexModule {
this.measures[measure.slug] = measure;
}
}
export default getModule(MeasureStoreModule);

View File

@ -1,24 +1,21 @@
import {
Action,
getModule,
Module,
Mutation,
MutationAction,
VuexModule
} from 'vuex-module-decorators';
import { LoginSubmit, User } from '@/models';
import api from '@/services/pm-api-client';
import store from '@/store';
import { logService } from '@/services/logging';
import { authStore } from '@/store';
@Module({ namespaced: true, name: 'user', store, dynamic: true })
class UserStoreModule extends VuexModule {
const log = logService.getLogger('/store-modules/user');
@Module({ namespaced: true, name: 'user' })
export class UserStoreModule extends VuexModule {
public user: User | null = null;
public users: User[] = [];
private log = logService.getLogger('/store-modules/user');
@MutationAction({ mutate: ['user']})
@MutationAction({ mutate: ['user'], rawError: true })
public async fetchUser() {
return { user: await api.getUser() };
}
@ -40,6 +37,3 @@ class UserStoreModule extends VuexModule {
return { users: this.users.filter((u) => u.id !== userId) };
}
}
export const userStore = getModule(UserStoreModule);
export default userStore;

View File

@ -1,11 +1,26 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { getModule } from 'vuex-module-decorators';
import { ApiTokenStoreModule } from './store-modules/api-token';
import { AuthStoreModule } from './store-modules/auth';
import { MeasureStoreModule } from './store-modules/measure';
import { UserStoreModule } from './store-modules/user';
Vue.use(Vuex);
export default new Vuex.Store({
export const store = new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: { }
modules: {
apiToken: ApiTokenStoreModule,
auth: AuthStoreModule,
measure: MeasureStoreModule,
user: UserStoreModule
}
});
export const apiTokenStore = getModule(ApiTokenStoreModule, store);
export const authStore = getModule(AuthStoreModule, store);
export const measureStore = getModule(MeasureStoreModule, store);
export const userStore = getModule(UserStoreModule, store);

View File

@ -1,6 +0,0 @@
<template>
<div class="dashboard">
<h1>Dashboard</h1>
</div>
</template>
<script lang="ts" src="./dashboard.ts"></script>

View File

@ -1,9 +1,13 @@
<template>
<div class=user-account>
<h1>Your Account</h1>
<section class=user-info>
<h2>About You</h2>
</section>
<fieldset>
<legend>About You</legend>
<label for=name>Name: </label>
<input name=name type=text :value="user.displayName"></input>
<label for=name>Email Address: </label>
<input name=name type=text :value="user.email"></input>
</fieldset>
<section class=api-tokens>
<h2>API Tokens</h2>
</section>

View File

@ -1,17 +0,0 @@
import { Component, Vue } from 'vue-property-decorator';
import { Route, RawLocation } from 'vue-router';
import userStore from '@/store-modules/user';
import { NavNext } from '@/types';
@Component({})
export default class Dashboard extends Vue {
public get user() {
return userStore.user;
}
public beforeRouteEnter<V extends Vue>( to: Route, from: Route, next: NavNext): void {
next();
}
}

View File

@ -1,7 +1,10 @@
import { Route } from 'vue-router';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { LoginSubmit } from '@/models';
import authStore from '@/store-modules/auth';
import { authStore } from '@/store';
import { logService } from '@/services/logging';
const log = logService.getLogger('/views/login');
@Component({})
export default class Login extends Vue {
@ -22,9 +25,11 @@ export default class Login extends Vue {
await authStore.login(this.loginForm);
this.$router.push({ path: this.redirect || '/' });
} catch (e) {
console.log(e);
if (e.response.status === 401) {
if (e.response && e.response.status === 401) {
this.flashMessage = 'invlid username or password';
} else {
this.flashMessage = 'unable to log you in';
log.error(e);
}
}
this.waiting = false;

View File

@ -1,5 +1,5 @@
import { Component, Vue } from 'vue-property-decorator';
import measureStore from '@/store-modules/measure';
import { measureStore } from '@/store';
@Component({
components: { }

View File

@ -3,8 +3,7 @@ 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 { measureStore, userStore } from '@/store';
// import UiIconButton from 'keen-ui/src/UiIconButton.vue';
library.add(faAngleDoubleLeft, faAngleDoubleRight, faHome, faPencilRuler, faThLarge, faUser);

View File

@ -1,4 +1,16 @@
import { Component, Vue } from 'vue-property-decorator';
import { apiTokenStore, userStore } from '@/store';
@Component({})
export default class UserAccount extends Vue {}
export default class UserAccount extends Vue {
private get user() { return userStore.user; }
private get apiTokens() { return apiTokenStore.apiTokens; }
private created() {
if (apiTokenStore.apiTokens.length === 0) {
apiTokenStore.fetchAllApiTokens();
}
}
}