diff --git a/package-lock.json b/package-lock.json index 916a192..7c7bb2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1356,7 +1356,6 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, - "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -2575,8 +2574,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "constants-browserify": { "version": "1.0.0", @@ -2971,8 +2969,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true + "dev": true }, "depd": { "version": "1.1.2", @@ -3290,6 +3287,14 @@ "is-arrayish": "^0.2.1" } }, + "error-stack-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.2.tgz", + "integrity": "sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw==", + "requires": { + "stackframe": "^1.0.4" + } + }, "es6-promise": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", @@ -3935,8 +3940,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -3957,14 +3961,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3979,20 +3981,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4109,8 +4108,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4122,7 +4120,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4137,7 +4134,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4145,14 +4141,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4171,7 +4165,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4252,8 +4245,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4265,7 +4257,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4351,8 +4342,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -4388,7 +4378,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4408,7 +4397,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4452,14 +4440,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -4468,7 +4454,6 @@ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "dev": true, - "optional": true, "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -4481,7 +4466,6 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, - "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -4519,8 +4503,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "3.0.0", @@ -5155,8 +5138,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true + "dev": true }, "has-value": { "version": "1.0.0", @@ -5874,8 +5856,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true, - "optional": true + "dev": true }, "is-windows": { "version": "1.0.2", @@ -6231,6 +6212,11 @@ "dev": true, "optional": true }, + "js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==" + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -6511,7 +6497,6 @@ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, - "optional": true, "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -6524,8 +6509,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "optional": true + "dev": true } } }, @@ -6801,8 +6785,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "map-visit": { "version": "1.0.0", @@ -7079,6 +7062,11 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -7427,7 +7415,6 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, - "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -8471,7 +8458,6 @@ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, - "optional": true, "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -8483,7 +8469,6 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, - "optional": true, "requires": { "graceful-fs": "^4.1.2", "pify": "^2.0.0", @@ -8494,8 +8479,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "optional": true + "dev": true } } }, @@ -8504,7 +8488,6 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, - "optional": true, "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -8515,7 +8498,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, - "optional": true, "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -8526,7 +8508,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, - "optional": true, "requires": { "pinkie-promise": "^2.0.0" } @@ -9669,6 +9650,45 @@ "safe-buffer": "^5.1.1" } }, + "stack-generator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.3.tgz", + "integrity": "sha512-kdzGoqrnqsMxOEuXsXyQTmvWXZmG0f3Ql2GDx5NtmZs59sT2Bt9Vdyq0XdtxUi58q/+nxtbF9KOQ9HkV1QznGg==", + "requires": { + "stackframe": "^1.0.4" + } + }, + "stackframe": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.0.4.tgz", + "integrity": "sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==" + }, + "stacktrace-gps": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz", + "integrity": "sha512-9o+nWhiz5wFnrB3hBHs2PTyYrS60M1vvpSzHxwxnIbtY2q9Nt51hZvhrG1+2AxD374ecwyS+IUwfkHRE/2zuGg==", + "requires": { + "source-map": "0.5.6", + "stackframe": "^1.0.4" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + } + } + }, + "stacktrace-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.0.tgz", + "integrity": "sha1-d2ymRqlbxsayuQd2U2p/xyxt21g=", + "requires": { + "error-stack-parser": "^2.0.1", + "stack-generator": "^2.0.1", + "stacktrace-gps": "^3.0.1" + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -9816,7 +9836,6 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, - "optional": true, "requires": { "is-utf8": "^0.2.0" } @@ -11156,7 +11175,6 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, - "optional": true, "requires": { "string-width": "^1.0.2 || 2" } diff --git a/package.json b/package.json index fbf44fa..ccaac1f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,10 @@ "hammerjs": "^2.0.8", "rxjs": "~6.3.3", "tslib": "^1.9.0", - "zone.js": "~0.8.26" + "zone.js": "~0.8.26", + "js-sha512": "latest", + "moment": "latest", + "stacktrace-js": "latest" }, "devDependencies": { "@angular-devkit/build-angular": "~0.13.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index fdcda44..01b99b0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,5 +1,5 @@ import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { ErrorHandler, NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -7,6 +7,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AngularFireModule } from '@angular/fire'; import { AngularFireDatabaseModule } from '@angular/fire/database'; import { environment } from '@env/environment'; +import { GlobalErrorHandler } from '@app/core/global-error-handler/global-error-handler.service'; +import { LogService } from '@app/core/services'; @NgModule({ declarations: [ @@ -18,7 +20,13 @@ import { environment } from '@env/environment'; AngularFireModule.initializeApp(environment.firebaseConfig), AngularFireDatabaseModule, ], - providers: [], + providers: [ + LogService, + { + provide: ErrorHandler, + useClass: GlobalErrorHandler + }, + ], bootstrap: [ AppComponent ] }) export class AppModule { diff --git a/src/app/core/global-error-handler/global-error-handler.service.spec.ts b/src/app/core/global-error-handler/global-error-handler.service.spec.ts new file mode 100644 index 0000000..aab0ab9 --- /dev/null +++ b/src/app/core/global-error-handler/global-error-handler.service.spec.ts @@ -0,0 +1,16 @@ +/* tslint:disable:no-unused-variable */ + +import { inject, TestBed } from '@angular/core/testing'; +import { GlobalErrorHandler } from './global-error-handler.service'; + +describe( 'Service: GlobalErrorHandler', () => { + beforeEach( () => { + TestBed.configureTestingModule( { + providers: [ GlobalErrorHandler ] + } ); + } ); + + it( 'should ...', inject( [ GlobalErrorHandler ], ( service: GlobalErrorHandler ) => { + expect( service ).toBeTruthy(); + } ) ); +} ); diff --git a/src/app/core/global-error-handler/global-error-handler.service.ts b/src/app/core/global-error-handler/global-error-handler.service.ts new file mode 100644 index 0000000..87e28bf --- /dev/null +++ b/src/app/core/global-error-handler/global-error-handler.service.ts @@ -0,0 +1,35 @@ +import { ErrorHandler, Injectable, Injector } from '@angular/core'; +import * as StackTrace from 'stacktrace-js'; + +import { LogService } from '@app/core/services'; + +@Injectable() +export class GlobalErrorHandler extends ErrorHandler { + constructor( private injector: Injector ) { + super(); + } + + public handleError( error ) { + const logService: LogService = this.injector.get( LogService ); + const message = error.message ? error.message : error.toString(); + + if ( error.status ) { + error = new Error( message ); + } + + StackTrace.fromError( error ).then( ( stackframes ) => { + const stackString = stackframes + .splice( 0, 10 ) + .map( ( sf ) => { + return sf.toString(); + } ) + .toString(); + + const errorTraceStr = `Error message: ${ message }. Stack trace: ${ stackString }`; + + logService.logError( errorTraceStr ); + + throw error; + } ); + } +} diff --git a/src/app/core/model/index.ts b/src/app/core/model/index.ts new file mode 100644 index 0000000..3f548db --- /dev/null +++ b/src/app/core/model/index.ts @@ -0,0 +1,2 @@ +export * from './storage/storage'; +export * from './user/user'; diff --git a/src/app/core/model/storage/storage.ts b/src/app/core/model/storage/storage.ts new file mode 100644 index 0000000..46d6a54 --- /dev/null +++ b/src/app/core/model/storage/storage.ts @@ -0,0 +1,5 @@ +export class Storage { + pageData: object; + sessionData: object; + localData: object; +} diff --git a/src/app/core/model/user/user.ts b/src/app/core/model/user/user.ts new file mode 100644 index 0000000..5793a30 --- /dev/null +++ b/src/app/core/model/user/user.ts @@ -0,0 +1,36 @@ +export class User { + timeGetToken: number; + token: string; + dataUser: DataUser; +} + +export class DataUser { + firstName: string; + lastName: string; + address: string; + postalCode: string; + city: string; + country: string; + phoneNumber: string; + email: string; + birthday: number; + licenseNumber: string; + licenseValidity: number; + licenseCategories: string[]; + + constructor() { + this.firstName = null; + this.lastName = null; + this.address = null; + this.postalCode = null; + this.city = null; + this.country = null; + this.phoneNumber = null; + this.email = null; + this.birthday = null; + this.licenseNumber = null; + this.licenseValidity = null; + this.licenseCategories = null; + } + +} diff --git a/src/app/core/services/auth/auth.service.spec.ts b/src/app/core/services/auth/auth.service.spec.ts new file mode 100644 index 0000000..0f0e5e8 --- /dev/null +++ b/src/app/core/services/auth/auth.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe( 'AuthService', () => { + beforeEach( () => TestBed.configureTestingModule( {} ) ); + + it( 'should be created', () => { + const service: AuthService = TestBed.get( AuthService ); + expect( service ).toBeTruthy(); + } ); +} ); diff --git a/src/app/core/services/auth/auth.service.ts b/src/app/core/services/auth/auth.service.ts new file mode 100644 index 0000000..8988993 --- /dev/null +++ b/src/app/core/services/auth/auth.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { Router } from '@angular/router'; +import * as sha512 from 'js-sha512'; + +// import * as config from '@assets/config.json'; + +import { User } from '@app/core/model'; +import { StorageService } from '@app/core/services/storage/storage.service'; +import { LogService } from '@app/core/services/log/log.service'; + +@Injectable( { providedIn: 'root' } ) +export class AuthService { + + public currentUser: Observable; + // public emailRegex = new RegExp( ); + private currentUserSubject: BehaviorSubject; + + constructor( private http: HttpClient, private router: Router, private stor: StorageService, private looger: LogService ) { + this.currentUserSubject = new BehaviorSubject( stor.getLocal( 'user' ) as User ); + this.currentUser = this.currentUserSubject.asObservable(); + } + + public get currentUserValue(): User { + if ( this.currentUserSubject.value && this.currentUserSubject.value.timeGetToken + 60 * 60 * 1000 < new Date().getTime() ) { + const data = new FormData(); + + data.append( 'auth_token', this.currentUserSubject.value.token ); + // this.http.post( environment.baseUrl + config.url.upauth, data ).subscribe( ( res ) => { + // if ( res.statusCode !== 200 ) { + // this.logout(); + // } else { + // this.currentUserSubject.value.timeGetToken = new Date().getTime(); + // this.stor.setLocal( 'user', this.currentUserSubject.value ); + // } + // } ); + } + return this.currentUserSubject.value; + } + + public login( username: string, password: string ): any { + const data: FormData = new FormData(); + + data.append( 'email', username ); + data.append( 'password', sha512.sha512( password ) ); + + // return this.http.post( environment.baseUrl + config.url.auth, data ) + // .pipe( map( result => { + // let user = new User(); + // + // this.looger.logInfo( result ); + // // login successful if there's a jwt token in the response + // if ( result && result.body && result.statusCode === 200 ) { + // + // user.token = result.body; + // user.timeGetToken = new Date().getTime(); + // + // // store user details and jwt token in local storage to keep user logged in between page refreshes + // this.stor.setLocal( 'user', user ); + // this.currentUserSubject.next( user ); + // } else { + // user = result.statusCode; + // } + // + // return user; + // } ) ); + } + + public logout(): void { + // remove user from local storage to log user out + this.stor.setLocal( 'user' ); + this.currentUserSubject.next( null ); + this.router.navigate( [ '/login' ] ); + } + + public createAccount( email: string, password: string, repeat: string ): any { + // const data: FormData = new FormData(); + + // if ( password === repeat && this.emailRegex.test( email ) ) { + // data.append( 'email', email ); + // data.append( 'password', sha512.sha512( password ) ); + + // return this.http.post( environment.baseUrl + config.url.register, data ); + // } + + return null; + } + +} diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts new file mode 100644 index 0000000..98c29a9 --- /dev/null +++ b/src/app/core/services/index.ts @@ -0,0 +1,2 @@ +export * from './auth/auth.service'; +export * from './log/log.service'; diff --git a/src/app/core/services/log/log-data.interface.ts b/src/app/core/services/log/log-data.interface.ts new file mode 100644 index 0000000..a122a7c --- /dev/null +++ b/src/app/core/services/log/log-data.interface.ts @@ -0,0 +1,8 @@ +export interface LogFields { + userId?: string; + elapsedTime?: number; + requestPath?: string; + environment?: string; + appVersion?: string; + url?: string; +} diff --git a/src/app/core/services/log/log.service.spec.ts b/src/app/core/services/log/log.service.spec.ts new file mode 100644 index 0000000..f2ed9b9 --- /dev/null +++ b/src/app/core/services/log/log.service.spec.ts @@ -0,0 +1,20 @@ +/* tslint:disable:no-unused-variable */ + +import { inject, TestBed } from '@angular/core/testing'; +import { LogService } from './log.service'; + +describe( 'Service: Log', () => { + beforeEach( () => { + TestBed.configureTestingModule( { + imports: [], + providers: [ LogService ] + } ); + } ); + + it( + 'should ...', + inject( [ LogService ], ( service: LogService ) => { + expect( service ).toBeTruthy(); + } ) + ); +} ); diff --git a/src/app/core/services/log/log.service.ts b/src/app/core/services/log/log.service.ts new file mode 100644 index 0000000..f407f38 --- /dev/null +++ b/src/app/core/services/log/log.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; +import { environment } from '@env/environment'; + +import { LogFields } from './log-data.interface'; +import { Logger } from './logger'; + +@Injectable( { + providedIn: 'root' +} ) +export class LogService { + + private logger: Logger; + + constructor() { + this.initialize(); + } + + public initialize() { + this.logger = new Logger( environment.appName ); + } + + public logHttpInfo( info: any, elapsedTime: number, requestPath: string ) { + // TODO: create and set correlation id + const url = location.href; + const logFields: LogFields = { + environment: environment.env, + // userId: this.userId, + requestPath, + elapsedTime, + url, + }; + + this.logger.log( 'Information', `${ info }`, logFields ); + } + + public logError( errorMsg: string ) { + const url = location.href; + + const logFields: LogFields = { + environment: environment.env, + // userId: this.userId, + requestPath: '', + elapsedTime: 0, + url: url, + }; + + this.logger.log( 'Error', errorMsg, logFields ); + } + + public logInfo( info: any ) { + const url = location.href; + + const logFields: LogFields = { + environment: environment.env, + // userId: this.userId, + requestPath: '', + elapsedTime: 0, + url, + }; + + this.logger.log( 'Information', info, logFields ); + } +} diff --git a/src/app/core/services/log/logger.ts b/src/app/core/services/log/logger.ts new file mode 100644 index 0000000..ae32912 --- /dev/null +++ b/src/app/core/services/log/logger.ts @@ -0,0 +1,140 @@ +import { environment } from '@env/environment'; +import * as moment from 'moment'; +import { Subject } from 'rxjs'; +import { debounceTime, filter } from 'rxjs/operators'; +import { LogFields } from './log-data.interface'; + +export type LogType = 'Error' | 'Information'; + +interface LogEntry { + type: LogType; + message: string; + data: LogFields; +} + +enum LoggerEvents { + Flush = 1 +} + +export class Logger { + private readonly APP_FIELD = 'Application'; + private readonly ENV_FIELD = 'Environment'; + private readonly VERSION_FIELD = 'Version'; + private readonly USER_NAME_FIELD = 'UserName'; + private readonly ELAPSED_MS_FIELD = 'ElapsedMilliseconds'; + private readonly REQUEST_PATH_FIELD = 'RequestPath'; + private readonly URL_FIELD = 'Url'; + private readonly APP_STATE_FIELD = 'AppState'; + + private buffer: LogEntry[] = []; + private flush = new Subject(); + + constructor( private appName: string, private logEndpoint?: string ) { + this.flush + .pipe( debounceTime( 5000 ), filter( ( event ) => event === LoggerEvents.Flush ) ) + .subscribe( () => this.flushBuffer() ); + } + + public log( type: LogType, message: string, data: LogFields ) { + this.buffer.push( { + type, + message, + data + } ); + this.flush.next( LoggerEvents.Flush ); + } + + private flushBuffer() { + const data = this.buffer.splice( 0 ); + + if ( data.length === 0 ) { + return; + } + + const body = data + .map( ( entry ) => this.buildLogString( entry ) ) + .reduce( ( sum, entry ) => ( sum += entry ), '' ); + + if ( !environment.production ) { + // This is nested to make sure we always end up in here when running locally + // as in do not && this to the above if... + // tslint:disable-next-line:no-console + console.log( { + body, + data + } ); + } else { + const xobj = new XMLHttpRequest(); + // tslint:disable-next-line:no-console + if ( this.logEndpoint ) { + xobj.onerror = ( err ) => console.error( err ); + xobj.open( 'POST', this.logEndpoint, true ); + xobj.send( body ); + } + } + } + + private buildLogString( entry: LogEntry ): string { + const index = this.buildIndexChunk(); + const body = this.buildBodyChunk( entry ); + + return `${ index }\n${ body }\n`; + } + + private buildIndexChunk() { + const date = moment(); + const index = { + index: { + _index: `logstash-${ date.format( 'YYYY.M.D' ) }`, + _type: 'logevent' + } + }; + + return JSON.stringify( index ); + } + + private buildBodyChunk( entry: LogEntry ) { + const { type, message, data } = entry; + const level = type; + const date = moment(); + const messageTemplate = this.getMessageTemplate(); + const fields = this.getFields( data ); + const body = { + '@timestamp': `${ date.toISOString() }`, + level, + messageTemplate, + message, + fields + }; + + return JSON.stringify( body ); + } + + private getMessageTemplate() { + const fields: string[] = [ + this.APP_FIELD, + this.ENV_FIELD, + this.VERSION_FIELD, + this.USER_NAME_FIELD, + this.ELAPSED_MS_FIELD, + this.REQUEST_PATH_FIELD, + this.URL_FIELD, + this.APP_STATE_FIELD + ]; + const template = fields.map( ( field ) => `{${ field }}` ).join( ' - ' ); + + return template; + } + + private getFields( data: LogFields ) { + return { + [ this.APP_FIELD ]: this.appName, + [ this.ENV_FIELD ]: data.environment, + [ this.VERSION_FIELD ]: data.appVersion, + [ this.USER_NAME_FIELD ]: data.userId, + [ this.ELAPSED_MS_FIELD ]: data.elapsedTime, + [ this.REQUEST_PATH_FIELD ]: data.requestPath, + [ this.URL_FIELD ]: data.url + }; + } +} diff --git a/src/app/core/services/storage/storage.service.spec.ts b/src/app/core/services/storage/storage.service.spec.ts new file mode 100644 index 0000000..fa30fb5 --- /dev/null +++ b/src/app/core/services/storage/storage.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { StorageService } from './storage.service'; + +describe( 'StorageService', () => { + beforeEach( () => TestBed.configureTestingModule( {} ) ); + + it( 'should be created', () => { + const service: StorageService = TestBed.get( StorageService ); + expect( service ).toBeTruthy(); + } ); +} ); diff --git a/src/app/core/services/storage/storage.service.ts b/src/app/core/services/storage/storage.service.ts new file mode 100644 index 0000000..69ad5db --- /dev/null +++ b/src/app/core/services/storage/storage.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@angular/core'; + +import { Storage } from '@app/core/model'; + +@Injectable() +export class StorageService { + + private data: Storage; + + constructor() { + const localData = localStorage.getItem( 'c2a' ); + const sessionData = sessionStorage.getItem( 'c2a' ); + + this.data = new Storage(); + this.data.pageData = {}; + this.data.sessionData = ( sessionData ? JSON.parse( sessionData ) : {} ); + this.data.localData = ( localData ? JSON.parse( localData ) : {} ); + } + + public getLocal( key: string ): string | number | boolean | object { + const value = this.data.localData[ key ]; + + if ( !value ) { + return null; + } + return value; + } + + public getSession( key: string ): string | number | boolean | object { + const value = this.data.sessionData[ key ]; + + if ( !value ) { + return null; + } + return value; + } + + public getPage( key: string ): string | number | boolean | object { + const value = this.data.pageData[ key ]; + + if ( !value ) { + return null; + } + return value; + } + + public setLocal( key: string, value?: any ): void { + let val: string | number | boolean; + + if ( typeof value === 'boolean' || 'string' || 'number' || 'object' ) { + val = value; + } else { + val = null; + } + + if ( val === null ) { + if ( this.data.localData[ key ] ) { + delete this.data.localData[ key ]; + } + } else { + this.data.localData[ key ] = val; + } + localStorage.setItem( 'c2a', JSON.stringify( this.data.localData ) ); + } + + public setSession( key: string, value?: any ): void { + let val: string | number | boolean; + + if ( typeof value === 'boolean' || 'string' || 'number' || 'object' ) { + val = value; + } else { + val = null; + } + + if ( val === null ) { + if ( this.data.sessionData[ key ] ) { + delete this.data.sessionData[ key ]; + } + } else { + this.data.sessionData[ key ] = val; + } + sessionStorage.setItem( 'c2a', JSON.stringify( this.data.localData ) ); + } + + public setPage( key: string, value?: any ): void { + let val: string | number | boolean; + + if ( typeof value === 'boolean' || 'string' || 'number' || 'object' ) { + val = value; + } else { + val = null; + } + + if ( val === null ) { + if ( this.data.pageData[ key ] ) { + delete this.data.pageData[ key ]; + } + } else { + this.data.pageData[ key ] = val; + } + } + +} diff --git a/src/assets/config.json b/src/assets/config.json new file mode 100644 index 0000000..e69de29 diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 9a290c9..977e218 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,5 +1,7 @@ export const environment = { - production: true, + appName: 'DiscoTrip', + production: false, + env: 'production', firebaseConfig: { apiKey: 'AIzaSyDe48emSyoXS3WV6vM4GjYiJwdcMhcspqY', authDomain: 'disco-trip.firebaseapp.com', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 13ab213..ad9dd14 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -3,7 +3,9 @@ // The list of file replacements can be found in `angular.json`. export const environment = { + appName: 'DiscoTrip', production: false, + env: 'local', firebaseConfig: { apiKey: 'AIzaSyDe48emSyoXS3WV6vM4GjYiJwdcMhcspqY', authDomain: 'disco-trip.firebaseapp.com',