diff --git a/web/src/app.ts b/web/src/app.ts index a5587cf..ecda6f4 100644 --- a/web/src/app.ts +++ b/web/src/app.ts @@ -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; } } } diff --git a/web/src/main.ts b/web/src/main.ts index ffc6ce7..74b78df 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -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'; diff --git a/web/src/models.d.ts b/web/src/models.d.ts index 8a70c04..331fb2b 100644 --- a/web/src/models.d.ts +++ b/web/src/models.d.ts @@ -3,6 +3,8 @@ export interface ApiToken { userId: string; name: string; value?: string; + created: Date; + expires?: Date; } export interface LoginSubmit { diff --git a/web/src/router.ts b/web/src/router.ts index c9e5df5..cf7622b 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -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', diff --git a/web/src/services/pm-api-client.ts b/web/src/services/pm-api-client.ts index b6ddb78..c0bb4de 100644 --- a/web/src/services/pm-api-client.ts +++ b/web/src/services/pm-api-client.ts @@ -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 { const resp = await this.http.get('/version'); return resp.data; @@ -33,129 +41,97 @@ export class PmApiClient { : Promise { const resp = await this.http.post('/auth-token', creds); + this.setAuthToken(resp.data); return resp.data; } public async getUser(): Promise { - - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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); diff --git a/web/src/store-modules/api-token.ts b/web/src/store-modules/api-token.ts new file mode 100644 index 0000000..cd04287 --- /dev/null +++ b/web/src/store-modules/api-token.ts @@ -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) }; + } +} diff --git a/web/src/store-modules/auth.ts b/web/src/store-modules/auth.ts index 3e3ec59..0a775fc 100644 --- a/web/src/store-modules/auth.ts +++ b/web/src/store-modules/auth.ts @@ -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; diff --git a/web/src/store-modules/measure.ts b/web/src/store-modules/measure.ts index e04f9b9..8391ace 100644 --- a/web/src/store-modules/measure.ts +++ b/web/src/store-modules/measure.ts @@ -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); diff --git a/web/src/store-modules/user.ts b/web/src/store-modules/user.ts index 9e57640..244be75 100644 --- a/web/src/store-modules/user.ts +++ b/web/src/store-modules/user.ts @@ -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; diff --git a/web/src/store.ts b/web/src/store.ts index 1180e10..9567e58 100644 --- a/web/src/store.ts +++ b/web/src/store.ts @@ -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); diff --git a/web/src/views/Dashboard.vue b/web/src/views/Dashboard.vue deleted file mode 100644 index b3ff725..0000000 --- a/web/src/views/Dashboard.vue +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/web/src/views/UserAccount.vue b/web/src/views/UserAccount.vue index c3be243..3b3f21b 100644 --- a/web/src/views/UserAccount.vue +++ b/web/src/views/UserAccount.vue @@ -1,9 +1,13 @@