diff --git a/web/package-lock.json b/web/package-lock.json index d4537e8..ba57f74 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14670,6 +14670,11 @@ "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.0.tgz", "integrity": "sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg==" }, + "vuex-module-decorators": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/vuex-module-decorators/-/vuex-module-decorators-0.9.8.tgz", + "integrity": "sha512-yyh9+0mO7NYZxw5BlXWNA/lHioVOUL0muDpJPL9ssAvje2PHQfFSOCSridK4vA3HasjyaGRtTPJKH7+7UCcpwg==" + }, "w3c-hr-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", diff --git a/web/package.json b/web/package.json index 122e297..05301d2 100644 --- a/web/package.json +++ b/web/package.json @@ -21,7 +21,8 @@ "vue-class-component": "^6.0.0", "vue-property-decorator": "^7.0.0", "vue-router": "^3.0.1", - "vuex": "^3.0.1" + "vuex": "^3.0.1", + "vuex-module-decorators": "^0.9.8" }, "devDependencies": { "@types/jest": "^23.1.4", diff --git a/web/public/img/mickey-open-door.gif b/web/public/img/mickey-open-door.gif new file mode 100644 index 0000000..c786702 Binary files /dev/null and b/web/public/img/mickey-open-door.gif differ diff --git a/web/src/app.scss b/web/src/app.scss index 8981593..6fd8e1c 100644 --- a/web/src/app.scss +++ b/web/src/app.scss @@ -1,5 +1,6 @@ @import '~@/styles/vars'; @import '~@/styles/reset'; +@import '~@/styles/ui-common'; @import url('https://fonts.googleapis.com/css?family=Dosis|Exo+2:600|Exo:700|Josefin+Sans:300|Montserrat:300|Raleway|Teko:500|Titillium+Web:200'); body { diff --git a/web/src/app.ts b/web/src/app.ts index a97a5f6..872a3d7 100644 --- a/web/src/app.ts +++ b/web/src/app.ts @@ -1,9 +1,42 @@ -import { Component, Vue } from 'vue-property-decorator'; +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 { User } from '@/models'; + @Component({ components: { NavBar } }) -export default class App extends Vue {} +export default class App extends Vue { + + private consoleLogAppender: ConsoleLogAppender; + private apiLogAppender: ApiLogAppender; + + constructor() { + 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); + this.apiLogAppender.batchSize = 1; + this.apiLogAppender.minimumTimePassedInSec = 5; + + logService.ROOT_LOGGER.appenders.push(this.apiLogAppender, this.consoleLogAppender); + + } + + private get user(): User | null { + return usersStore.user; + } + + @Watch('user') + private onUserChanged(val: User | null , oldVal: User | null) { + if (val) { this.apiLogAppender.authToken = val.authToken; } + } +} diff --git a/web/src/class-component-hooks.ts b/web/src/class-component-hooks.ts new file mode 100644 index 0000000..9fb0383 --- /dev/null +++ b/web/src/class-component-hooks.ts @@ -0,0 +1,7 @@ +import Component from 'vue-class-component'; + +Component.registerHooks([ + 'beforeRouteEnter', + 'beforeRouteLeave', + 'beforeRouteUpdate' +]); diff --git a/web/src/main.ts b/web/src/main.ts index ffbe476..ffc6ce7 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,18 +1,14 @@ +import './class-component-hooks'; + import Vue from 'vue'; import App from './App.vue'; import router from './router'; import store from './store'; -import './registerServiceWorker'; -import { LogService, LogLevel, ApiAppender, ConsoleAppender } from './services/logging'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; -Vue.component('fa-icon', FontAwesomeIcon); +import './registerServiceWorker'; -LogService.getRootLogger().appenders.push( - // TODO: prod/dev config settings for logging? - new ConsoleAppender(LogLevel.DEBUG), - new ApiAppender(process.env.VUE_APP_PM_API_BASE + '/log/batch', '', LogLevel.WARN) -); +Vue.component('fa-icon', FontAwesomeIcon); new Vue({ router, diff --git a/web/src/models.d.ts b/web/src/models.d.ts new file mode 100644 index 0000000..ef65476 --- /dev/null +++ b/web/src/models.d.ts @@ -0,0 +1,40 @@ +export interface ApiToken { + id: string; + userId: string; + name: string; + value?: string; +} + +export interface LoginSubmit { + email: string; + password: string; +} + +export interface Measure { + id: string; + userId: string; + slug: string; + name: string; + description: string; + domainSource?: string; + domainUnites: string; + rangeSource?: string; + rangeUnits: string; + analysis: string[]; +} + +export interface Measurement { + id: string; + measureId: string; + value: number; + timestamp: Date; + extData: object; +} + +export interface User { + id: string; + displayName: string; + email: string; + isAdmin: boolean; + authToken?: string; +} diff --git a/web/src/models/api-token.ts b/web/src/models/api-token.ts deleted file mode 100644 index ee81377..0000000 --- a/web/src/models/api-token.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface ApiToken { - id: string; - userId: string; - name: string; - value?: string; -} diff --git a/web/src/models/index.ts b/web/src/models/index.ts deleted file mode 100644 index 8720cfd..0000000 --- a/web/src/models/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import ApiToken from './api-token'; -import Measure from './measure'; -import Measurement from './measurement'; -import User from './user'; - -export { ApiToken, Measure, Measurement, User }; diff --git a/web/src/models/measure.ts b/web/src/models/measure.ts deleted file mode 100644 index 1eb36de..0000000 --- a/web/src/models/measure.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default interface Measure { - id: string; - userId: string; - slug: string; - name: string; - description: string; - domainSource?: string; - domainUnites: string; - rangeSource?: string; - rangeUnits: string; - analysis: string[]; -} diff --git a/web/src/models/measurement.ts b/web/src/models/measurement.ts deleted file mode 100644 index 75c5122..0000000 --- a/web/src/models/measurement.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default interface Measurement { - id: string; - measureId: string; - value: number; - timestamp: Date; - extData: object; -} diff --git a/web/src/models/user.ts b/web/src/models/user.ts deleted file mode 100644 index 2cd513d..0000000 --- a/web/src/models/user.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default interface User { - id: string; - displayName: string; - email: string; - isAdmin: boolean; - authToken?: string; -} diff --git a/web/src/router.ts b/web/src/router.ts index f0ed218..7eb2205 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -6,8 +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 userService from '@/services/user'; +import userStore from '@/store-modules/user'; Vue.use(Router); @@ -54,10 +53,9 @@ const router = new Router({ // Auth filter router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.requiresAuth)) { - if (!userService.isAuthed()) { - next({ - path: '/login', - params: { nextUrl: to.fullPath } }); + if (!userStore.user || !userStore.user.authToken) { + next({ name: 'login' }); + // params: { redirect: to.path } }); } else { next(); } // if authed } else { next(); } // if auth required }); diff --git a/web/src/services/logging/api-appender.ts b/web/src/services/logging/api-log-appender.ts similarity index 71% rename from web/src/services/logging/api-appender.ts rename to web/src/services/logging/api-log-appender.ts index 53ab4fd..19de0fe 100644 --- a/web/src/services/logging/api-appender.ts +++ b/web/src/services/logging/api-log-appender.ts @@ -11,7 +11,7 @@ interface ApiMessage { stacktrace: string; timestamp: string; } -export class ApiAppender implements LogAppender { +export class ApiLogAppender implements LogAppender { public batchSize = 10; public minimumTimePassedInSec = 60; public maximumTimePassedInSec = 120; @@ -20,17 +20,17 @@ export class ApiAppender implements LogAppender { private msgBuffer: ApiMessage[] = []; private lastSent = 0; - constructor(public readonly apiEndpoint: string, public authToken: string, public threshold?: LogLevel) { + constructor(public readonly apiEndpoint: string, public authToken?: string, public threshold?: LogLevel) { setInterval(this.checkPost, 1000); } - public appendMessage(logger: Logger, msg: LogMessage): void { + public appendMessage(msg: LogMessage): void { if (this.threshold && msg.level < this.threshold) { return; } this.msgBuffer.push({ level: LogLevel[msg.level], message: msg.message, - scope: logger.name, + scope: msg.scope, stacktrace: msg.stacktrace, timestamp: msg.timestamp.toISOString() }); @@ -49,17 +49,18 @@ export class ApiAppender implements LogAppender { } private doPost() { - if (this.msgBuffer.length === 0) { return; } + if (this.msgBuffer.length > 0 && this.authToken) { - this.http.post(this.apiEndpoint, this.msgBuffer, - { headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.authToken}` - }}); + this.http.post(this.apiEndpoint, this.msgBuffer, + { headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.authToken}` + }}); - this.lastSent = Date.now(); - this.msgBuffer = []; + this.lastSent = Date.now(); + this.msgBuffer = []; + } } } -export default ApiAppender; +export default ApiLogAppender; diff --git a/web/src/services/logging/console-appender.ts b/web/src/services/logging/console-log-appender.ts similarity index 63% rename from web/src/services/logging/console-appender.ts rename to web/src/services/logging/console-log-appender.ts index 0dbabff..6a07a1d 100644 --- a/web/src/services/logging/console-appender.ts +++ b/web/src/services/logging/console-log-appender.ts @@ -3,16 +3,17 @@ import { LogMessage, LogLevel} from './log-message'; import Logger from './logger'; import LogAppender from './log-appender'; -export class ConsoleAppender implements LogAppender { +export class ConsoleLogAppender implements LogAppender { constructor(public threshold?: LogLevel) {} - public appendMessage(logger: Logger, msg: LogMessage): void { + public appendMessage(msg: LogMessage): void { if (this.threshold && msg.level < this.threshold) { return; } let logMethod = console.log; switch (msg.level) { - case LogLevel.ALL: logMethod = console.trace; break; + case LogLevel.ALL: logMethod = console.log; break; case LogLevel.DEBUG: logMethod = console.debug; break; + case LogLevel.LOG: logMethod = console.log; break; case LogLevel.INFO: logMethod = console.info; break; case LogLevel.WARN: logMethod = console.warn; break; case LogLevel.ERROR: @@ -20,11 +21,11 @@ export class ConsoleAppender implements LogAppender { } if (msg.error) { - logMethod(logger.name, msg.message, msg.error); + logMethod(`[${msg.scope}]:`, msg.message, msg.error); } else { - logMethod(logger.name, msg.message, msg.stacktrace); + logMethod(`[${msg.scope}]:`, msg.message, msg.stacktrace); } } } -export default ConsoleAppender; +export default ConsoleLogAppender; diff --git a/web/src/services/logging/index.ts b/web/src/services/logging/index.ts index e961f8c..aa1bc7f 100644 --- a/web/src/services/logging/index.ts +++ b/web/src/services/logging/index.ts @@ -1,5 +1,6 @@ export * from './log-message'; export * from './log-appender'; export * from './log-service'; -export * from './console-appender'; -export * from './api-appender'; +export * from './console-log-appender'; +export * from './api-log-appender'; +export * from './logger'; diff --git a/web/src/services/logging/log-appender.ts b/web/src/services/logging/log-appender.ts index c3310d8..8b594c1 100644 --- a/web/src/services/logging/log-appender.ts +++ b/web/src/services/logging/log-appender.ts @@ -1,5 +1,5 @@ import { LogLevel, LogMessage } from './log-message'; import Logger from './logger'; export default interface LogAppender { - appendMessage(logger: Logger, message: LogMessage): void; + appendMessage(message: LogMessage): void; } diff --git a/web/src/services/logging/log-message.ts b/web/src/services/logging/log-message.ts index fefd46b..f5d0a5c 100644 --- a/web/src/services/logging/log-message.ts +++ b/web/src/services/logging/log-message.ts @@ -1,6 +1,7 @@ -export enum LogLevel { ALL = 0, DEBUG, INFO, WARN, ERROR, FATAL } +export enum LogLevel { ALL = 0, DEBUG, LOG, INFO, WARN, ERROR, FATAL } export interface LogMessage { + scope: string; level: LogLevel; message: string; stacktrace: string; diff --git a/web/src/services/logging/log-service.ts b/web/src/services/logging/log-service.ts index 531769e..70117fe 100644 --- a/web/src/services/logging/log-service.ts +++ b/web/src/services/logging/log-service.ts @@ -2,14 +2,24 @@ import { LogLevel } from './log-message'; import Logger from './logger'; import { default as Axios, AxiosInstance } from 'axios'; +const ROOT_LOGGER_NAME = 'ROOT'; + /* tslint:disable:max-classes-per-file*/ export class LogService { - public static getRootLogger(): Logger { return Logger.ROOT_LOGGER; } - - private loggers: { [key: string]: Logger } = { }; + private loggers: { [key: string]: Logger }; private http: AxiosInstance = Axios.create(); + public get ROOT_LOGGER() { + return this.loggers[ROOT_LOGGER_NAME]; + } + + public constructor() { + this.loggers = {}; + this.loggers[ROOT_LOGGER_NAME] = + new Logger(ROOT_LOGGER_NAME, undefined, LogLevel.ALL); + } + public getLogger(name: string, threshold?: LogLevel): Logger { if (this.loggers.hasOwnProperty(name)) { return this.loggers[name]; } @@ -17,12 +27,12 @@ export class LogService { const parentLoggerName = Object.keys(this.loggers) .filter((n: string) => name.startsWith(n)) - .reduce((acc: string, cur: string) => acc.length > cur.length ? acc : cur); + .reduce((acc: string, cur: string) => acc.length > cur.length ? acc : cur, ''); if (parentLoggerName) { parentLogger = this.loggers[parentLoggerName]; } else { - parentLogger = Logger.ROOT_LOGGER; + parentLogger = this.ROOT_LOGGER; } this.loggers[name] = parentLogger.createChildLogger(name, threshold); @@ -30,4 +40,5 @@ export class LogService { } } -export default new LogService(); +export const logService = new LogService(); +export default logService; diff --git a/web/src/services/logging/logger.ts b/web/src/services/logging/logger.ts index 06e99aa..f56c31a 100644 --- a/web/src/services/logging/logger.ts +++ b/web/src/services/logging/logger.ts @@ -1,23 +1,29 @@ import { LogMessage, LogLevel } from './log-message'; import LogAppender from './log-appender'; -export default class Logger { - - public static readonly ROOT_LOGGER = new Logger('ROOT', undefined, LogLevel.ALL); +export class Logger { public appenders: LogAppender[] = []; - protected constructor(public readonly name: string, private parentLogger?: Logger, public threshold?: LogLevel) { } + public constructor( + public readonly name: string, + private parentLogger?: Logger, + public threshold?: LogLevel) { } public createChildLogger(name: string, threshold?: LogLevel): Logger { return new Logger(name, this, threshold); } - public log(level: LogLevel, message: (Error | string), stacktrace?: string): void { + public doLog(level: LogLevel, message: (Error | string), stacktrace?: string): void { if (level < this.getEffectiveThreshold()) { return; } - const logMsg: LogMessage = { level, message: '', stacktrace: '', timestamp: new Date() }; + const logMsg: LogMessage = { + scope: this.name, + level, + message: '', + stacktrace: '', + timestamp: new Date() }; if (typeof message === 'string') { logMsg.message = message; @@ -28,29 +34,43 @@ export default class Logger { logMsg.stacktrace = message.stack == null ? '' : message.stack; } - this.appenders.forEach((app) => { - app.appendMessage(this, logMsg); - }); + this.sendToAppenders(logMsg); + } + + public trace(message: (Error | string), stacktrace?: string): void { + this.doLog(LogLevel.ALL, message, stacktrace); } public debug(message: (Error | string), stacktrace?: string): void { - this.log(LogLevel.DEBUG, message, stacktrace); + this.doLog(LogLevel.DEBUG, message, stacktrace); + } + + public log(message: string, stacktrace?: string): void { + this.doLog(LogLevel.LOG, message, stacktrace); } public info(message: string, stacktrace?: string): void { - this.log(LogLevel.INFO, message, stacktrace); + this.doLog(LogLevel.INFO, message, stacktrace); } public warn(message: string, stacktrace?: string): void { - this.log(LogLevel.WARN, message, stacktrace); + this.doLog(LogLevel.WARN, message, stacktrace); } public error(message: string, stacktrace?: string): void { - this.log(LogLevel.ERROR, message, stacktrace); + this.doLog(LogLevel.ERROR, message, stacktrace); } public fatal(message: string, stacktrace?: string): void { - this.log(LogLevel.FATAL, message, stacktrace); + this.doLog(LogLevel.FATAL, message, stacktrace); + } + + protected sendToAppenders(logMsg: LogMessage) { + this.appenders.forEach((app) => { + app.appendMessage(logMsg); + }); + + if (this.parentLogger) { this.parentLogger.sendToAppenders(logMsg); } } protected getEffectiveThreshold(): LogLevel { @@ -63,4 +83,4 @@ export default class Logger { } - +export default Logger; diff --git a/web/src/services/pm-api-client.ts b/web/src/services/pm-api-client.ts index cf6f92b..f82c08d 100644 --- a/web/src/services/pm-api-client.ts +++ b/web/src/services/pm-api-client.ts @@ -1,9 +1,12 @@ import { default as Axios, AxiosInstance } from 'axios'; -import { ApiToken, Measure, Measurement, User } from '@/models'; +import { ApiToken, LoginSubmit, Measure, Measurement, User } from '@/models'; +import { Logger, logService } from '@/services/logging'; import merge from 'lodash.merge'; export class PmApiClient { private http: AxiosInstance; + private authToken?: string; + private log: Logger; constructor(apiBase: string) { this.http = Axios.create({ @@ -16,161 +19,200 @@ export class PmApiClient { } */ }); + + this.log = logService.getLogger('services/pm-api-client'); + this.log.trace('Initialized PmApiClient'); } - public version(): Promise { - return this.http - .get('/version') - .then((resp) => resp.data); + public setAuthToken(t: string) { this.authToken = t; } + + public async version(): Promise { + const resp = await this.http.get('/version'); + return resp.data; } - public createAuthToken(email: string, password: string): Promise { - return this.http - .post('/auth-token', { email, password }) - .then((resp) => resp.data); + public async createAuthToken(creds: LoginSubmit) + : Promise { + + const resp = await this.http.post('/auth-token', creds); + return resp.data; } - public getUser(authToken: string): Promise { - return this.http - .get('/user', { headers: { Authorization: 'Bearer ' + authToken }}) - .then((resp) => merge(resp.data, { authToken }) ); + public async getUser(authToken: string): Promise { + + const resp = await this.http.get('/user', + { headers: { Authorization: 'Bearer ' + authToken }}); + + return merge(resp.data, { authToken }) ; } - public getAllUsers(user: User): Promise { - return this.http - .get('/users', { headers: this.authHeader(user) }) - .then((resp) => resp.data); + public async getAllUsers(): Promise { + const resp = await this.http.get('/users', + { headers: this.authHeader() }); + + return resp.data; } - public getUserById(curUser: User, reqUserId: string): Promise { - return this.http - .get(`/users/${reqUserId}`, { headers: this.authHeader(curUser) }) - .then((resp) => resp.data); + public async getUserById(reqUserId: string): Promise { + const resp = await this.http.get(`/users/${reqUserId}`, + { headers: this.authHeader() }); + + return resp.data; } - public createUser(curUser: User, newUser: User): Promise { - return this.http - .post('/users', newUser, { headers: this.authHeader(curUser) }) - .then((resp) => resp.data); + public async createUser(newUser: User): Promise { + const resp = await this.http.post('/users', + newUser, { headers: this.authHeader() }); + + return resp.data; } - public deleteUser(curUser: User, toDeleteUserId: string): Promise { - return this.http - .delete(`/users/${toDeleteUserId}`, { headers: this.authHeader(curUser) }) - .then((resp) => true); + public async deleteUser(toDeleteUserId: string) + : Promise { + + await this.http.delete(`/users/${toDeleteUserId}`, + { headers: this.authHeader() }); + + return true; } - public changePwd(user: User, oldPassword: string, newPassword: string): Promise { - return this.http - .post( - '/change-pwd', - { oldPassword, newPassword }, - { headers: this.authHeader(user) }) - .then((resp) => true); + public async changePwd(oldPassword: string, newPassword: string) + : Promise { + + await this.http.post('/change-pwd', + { oldPassword, newPassword }, { headers: this.authHeader() }); + + return true; } - public changePwdForUser(user: User, forUserId: string, newPassword: string): Promise { - return this.http - .post( - `/change-pwd/${forUserId}`, - { newPassword }, - { headers: this.authHeader(user) }) - .then((resp) => true); + public async changePwdForUser(forUserId: string, newPassword: string) + : Promise { + + await this.http.post(`/change-pwd/${forUserId}`, + { newPassword }, { headers: this.authHeader() }); + + return true; } - public createApiToken(user: User, token: ApiToken): Promise { - return this.http - .post(`/api-tokens`, token, { headers: this.authHeader(user) }) - .then((resp) => resp.data); + public async createApiToken(token: ApiToken) + : Promise { + + const resp = await this.http.post(`/api-tokens`, + token, { headers: this.authHeader() }); + + return resp.data; } - public getAllApiTokens(user: User): Promise { - return this.http - .get('/api-tokens', { headers: this.authHeader(user) }) - .then((resp) => resp.data); + public async getAllApiTokens(): Promise { + const resp = await this.http.get('/api-tokens', + { headers: this.authHeader() }); + + return resp.data; } - public getApiToken(user: User, tokenId: string): Promise { - return this.http - .get(`/api-tokens/${tokenId}`, { headers: this.authHeader(user) }) - .then((resp) => resp.data); + public async getApiToken(tokenId: string): Promise { + const resp = await this.http.get(`/api-tokens/${tokenId}`, + { headers: this.authHeader() }); + + return resp.data; } - public deleteApiToken(user: User, tokenId: string): Promise { - return this.http - .delete(`/api-tokens/${tokenId}`, { headers: this.authHeader(user) }) - .then((resp) => true); + public async deleteApiToken(tokenId: string): Promise { + const resp = await this.http.delete(`/api-tokens/${tokenId}`, + { headers: this.authHeader() }); + + return true; } - public getAllMeasures(user: User): Promise { - return this.http - .get(`/measures`, { headers: this.authHeader(user) }) - .then((resp) => resp.data); + public async getAllMeasures(): Promise { + const resp = await this.http.get(`/measures`, + { headers: this.authHeader() }); + + return resp.data; } - public createMeasure(user: User, measure: Measure): Promise { - return this.http - .post(`/measures`, measure, { headers: this.authHeader(user) }) - .then((resp) => resp.data); + public async createMeasure(measure: Measure): Promise { + const resp = await this.http.post(`/measures`, + measure, { headers: this.authHeader() }); + + return resp.data; } - public getMeasure(user: User, slug: string): Promise { - return this.http - .get(`/measures/${slug}`, { headers: this.authHeader(user) }) - .then((resp) => resp.data); + public async getMeasure(slug: string): Promise { + const resp = await this.http.get(`/measures/${slug}`, + { headers: this.authHeader() }); + + return resp.data; } - public deleteMeasure(user: User, slug: string): Promise { - return this.http - .delete(`/measures/${slug}`, { headers: this.authHeader(user) }) - .then((resp) => true); + public async deleteMeasure(slug: string): Promise { + const resp = await this.http.delete(`/measures/${slug}`, + { headers: this.authHeader() }); + + return true; } - public getMeasurements(user: User, measureSlug: string): - Promise { + public async getMeasurements(measureSlug: string) + : Promise { - return this.http - .get(`/measure/${measureSlug}`, { headers: this.authHeader(user) }) - .then((resp) => resp.data); + const resp = await this.http.get(`/measure/${measureSlug}`, + { headers: this.authHeader() }); + + return resp.data; } - public createMeasurement(user: User, measureSlug: string, - measurement: Measurement): Promise { - return this.http - .post(`/measure/${measureSlug}`, - measurement, - { headers: this.authHeader(user) }) - .then((resp) => resp.data); + public async createMeasurement( + measureSlug: string, + measurement: Measurement) + : Promise { + + const resp = await this.http.post(`/measure/${measureSlug}`, + measurement, { headers: this.authHeader() }); + + return resp.data; } - public getMeasurement(user: User, measureSlug: string, measurementId: string): - Promise { + public async getMeasurement( + measureSlug: string, + measurementId: string) + : Promise { - return this.http - .get(`/measure/${measureSlug}/${measurementId}`, - { headers: this.authHeader(user) }) - .then((resp) => resp.data); + const resp = await this.http.get(`/measure/${measureSlug}/${measurementId}`, + { headers: this.authHeader() }); + + return resp.data; } - public updateMeasurement(user: User, measureSlug: string, - measurement: Measurement): Promise { - return this.http - .put(`/measure/${measureSlug}/${measurement.id}`, - measurement, - { headers: this.authHeader(user) }) - .then((resp) => resp.data) ; + public async updateMeasurement( + measureSlug: string, + measurement: Measurement) + : Promise { + + const resp = await this.http.put(`/measure/${measureSlug}/${measurement.id}`, + measurement, { headers: this.authHeader() }); + + return resp.data; } - public deleteMeasurement(user: User, measureSlug: string, measurementId: string): Promise { - return this.http - .delete(`/measure/${measureSlug}/${measurementId}`, - {headers: this.authHeader(user) }) - .then((resp) => true); + public async deleteMeasurement( + measureSlug: string, + measurementId: string) + : Promise { + + const resp = await this.http.delete(`/measure/${measureSlug}/${measurementId}`, + {headers: this.authHeader() }); + + return true; } - private authHeader(user: User): { [key: string]: string } { - return { Authorization: 'Bearer ' + user.authToken }; + private authHeader(): { [key: string]: string } { + if (this.authToken) { + return { Authorization: 'Bearer ' + this.authToken }; + } else { + throw new Error('no authenticated user'); + } } } -export default new PmApiClient(process.env.VUE_APP_PM_API_BASE); +export const api = new PmApiClient(process.env.VUE_APP_PM_API_BASE); +export default api; diff --git a/web/src/services/user.ts b/web/src/services/user.ts deleted file mode 100644 index ac1fdd3..0000000 --- a/web/src/services/user.ts +++ /dev/null @@ -1,29 +0,0 @@ -import User from '@/models/user.ts'; -import { default as apiClient, PmApiClient } from '@/services/pm-api-client.ts'; - -export class UserService { - private user?: User = undefined; - - constructor(private api: PmApiClient) { } - - public isAuthed(): boolean { - return this.user != null && this.user.authToken != null; - } - - public getUser(): User | undefined { return this.user; } - - public authUser(email: string, password: string): Promise { - return this.api.createAuthToken(email, password) - .then((token) => this.api.getUser(token)); - } - - public getUserById(reqUserId: string): Promise { - if (this.user == null) { - return Promise.reject(new Error('no currently authenticated user')); - } else { - return this.api.getUserById(this.user, reqUserId); - } - } -} - -export default new UserService(apiClient); diff --git a/web/src/store-modules/user.ts b/web/src/store-modules/user.ts new file mode 100644 index 0000000..6312f9b --- /dev/null +++ b/web/src/store-modules/user.ts @@ -0,0 +1,52 @@ +import { + Action, + getModule, + Module, + Mutation, + MutationAction, + VuexModule +} from 'vuex-module-decorators'; +import { LoginSubmit, User } from '@/models'; +import store from '@/store'; +import api from '@/services/pm-api-client'; +import { logService } from '@/services/logging'; + +@Module({ namespaced: true, name: 'user', store, dynamic: true }) +class UserStoreModule extends VuexModule { + public user: User | null = null; + public users: User[] = []; + + 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: ['users'] }) + public async fetchAllUsers() { + const users = await api.getAllUsers(); + return { users }; + } + + @MutationAction({ mutate: ['users'] }) + public async createUser(user: User) { + const newUser = await api.createUser(user); + return { users: this.users.concat([user]) }; + } + + @MutationAction({ mutate: ['users'] }) + public async deleteUser(userId: string) { + 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); diff --git a/web/src/store.ts b/web/src/store.ts index ce737b9..1180e10 100644 --- a/web/src/store.ts +++ b/web/src/store.ts @@ -4,13 +4,8 @@ import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ - state: { - - }, - mutations: { - - }, - actions: { - - } + state: {}, + mutations: {}, + actions: {}, + modules: { } }); diff --git a/web/src/styles/ui-common.scss b/web/src/styles/ui-common.scss new file mode 100644 index 0000000..362aa42 --- /dev/null +++ b/web/src/styles/ui-common.scss @@ -0,0 +1,16 @@ +.btn, +.btn-action { + border: 0; + border-radius: .25em; + cursor: pointer; + padding: .5em 1em; +} + +.btn-action { + background-color: $color2; + position: relative; + + &:hover { + background-color: darken($color2, 5%); + } +} diff --git a/web/src/styles/vars.scss b/web/src/styles/vars.scss index 6ec9245..0f9fbe5 100644 --- a/web/src/styles/vars.scss +++ b/web/src/styles/vars.scss @@ -14,8 +14,8 @@ $color3: rgba(255, 253, 130, 1); $color4: rgba(255, 155, 113, 1); $color5: rgba(232, 72, 85, 1); -$bg-primary: #333; -$fg-primary: #d8d8e0; +$fg-primary: #222; +$bg-primary: #f0f0f0; $screen-x-small: 320px; $screen-small: 640px; diff --git a/web/src/types/index.d.ts b/web/src/types/index.d.ts new file mode 100644 index 0000000..bc874d7 --- /dev/null +++ b/web/src/types/index.d.ts @@ -0,0 +1 @@ +export * from './nav-next'; diff --git a/web/src/types/nav-next.d.ts b/web/src/types/nav-next.d.ts new file mode 100644 index 0000000..606bdd2 --- /dev/null +++ b/web/src/types/nav-next.d.ts @@ -0,0 +1,4 @@ +import { Vue } from 'vue-property-decorator'; +import { RawLocation } from 'vue-router'; + +export type NavNext = (to?: RawLocation | false | ((vm: Vue) => any) | void) => void; diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue new file mode 100644 index 0000000..fb884a9 --- /dev/null +++ b/web/src/views/Login.vue @@ -0,0 +1,25 @@ + + + diff --git a/web/src/views/NavBar.vue b/web/src/views/NavBar.vue index 4b6b2d9..14263af 100644 --- a/web/src/views/NavBar.vue +++ b/web/src/views/NavBar.vue @@ -1,5 +1,5 @@