From ae14a3361620586cfa3644b806c7c39b330eda1c Mon Sep 17 00:00:00 2001 From: William Brawner Date: Tue, 26 Jan 2021 21:00:15 -0700 Subject: [PATCH] Use bearer instead of basic auth --- .vscode/launch.json | 7 ++++ package-lock.json | 5 --- package.json | 1 - src/app/app.component.ts | 42 +++++++++++++--------- src/app/app.module.ts | 3 +- src/app/shared/auth.interceptor.ts | 9 +++-- src/app/shared/twigs.http.service.ts | 48 ++++++++++++++------------ src/app/users/login/login.component.ts | 8 +++-- src/app/users/user.ts | 5 +++ src/environments/environment.ts | 2 +- 10 files changed, 75 insertions(+), 55 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 174a9da..e828491 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,13 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Launch Chrome", + "request": "launch", + "type": "pwa-chrome", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}" + }, { "name": "ng serve", "type": "node", diff --git a/package-lock.json b/package-lock.json index 9d0a940..27a57a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8470,11 +8470,6 @@ "resolved": "https://registry.npmjs.org/ng2-currency-mask/-/ng2-currency-mask-9.0.2.tgz", "integrity": "sha512-ydtSTEqXR32mHsCElDvmSEywmN45Dam4aWUhoYJl3eEm9T6QWgA3DzHxNiFcur2kH94gomdvlQAB490CcObGMg==" }, - "ngx-cookie-service": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-2.4.0.tgz", - "integrity": "sha512-uR/6mCQ1t+XY5G1/irqRhoEddx1PPtmz7JHM/2nn5yQmicnj+n48x8C2PMxwaYDHKRh7QPQ9G5scR36Mmdz09A==" - }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/package.json b/package.json index b6c2e66..faa22ec 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "dexie": "^2.0.4", "ng2-charts": "^2.4.2", "ng2-currency-mask": "^9.0.2", - "ngx-cookie-service": "^2.4.0", "rxjs": "^6.6.3", "tslib": "^2.0.0", "zone.js": "~0.10.3" diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2f9fc22..8279630 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,8 +1,7 @@ -import { Component, Inject, ApplicationRef, ChangeDetectorRef } from '@angular/core'; +import { Component, Inject, ApplicationRef, ChangeDetectorRef, OnInit } from '@angular/core'; import { Location } from '@angular/common'; import { User } from './users/user'; import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service'; -import { CookieService } from 'ngx-cookie-service'; import { SwUpdate } from '@angular/service-worker'; import { first, filter, map } from 'rxjs/operators'; import { interval, concat, BehaviorSubject } from 'rxjs'; @@ -14,7 +13,7 @@ import { Actionable, isActionable } from './shared/actionable'; templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) -export class AppComponent { +export class AppComponent implements OnInit { public title = 'Twigs'; public backEnabled = false; public user = new BehaviorSubject(null); @@ -26,41 +25,50 @@ export class AppComponent { constructor( @Inject(TWIGS_SERVICE) private twigsService: TwigsService, private location: Location, - private cookieService: CookieService, private router: Router, - private activatedRoute: ActivatedRoute, private appRef: ApplicationRef, private updates: SwUpdate, private changeDetector: ChangeDetectorRef, - ) { + private storage: Storage + ){} + + ngOnInit(): void { const unauthenticatedRoutes = [ + '', '/', '/login', '/register' ] - if (this.cookieService.check('Authorization')) { - this.twigsService.getProfile().subscribe(user => { - this.user.next(user); - if (this.router.url == '/') { + let auth = this.storage.getItem('Authorization'); + let savedUser = JSON.parse(this.storage.getItem('user')) as User; + if (auth && auth.length == 255) { + if (savedUser) { + this.user.next(savedUser); + } + this.twigsService.getProfile().subscribe(fetchedUser => { + this.storage.setItem('user', JSON.stringify(fetchedUser)); + this.user.next(fetchedUser); + if (unauthenticatedRoutes.indexOf(this.location.path()) != -1) { + //TODO: Save last opened budget and redirect to there instead of the main list this.router.navigateByUrl("/budgets"); } }); - } else if (unauthenticatedRoutes.indexOf(this.router.url) == -1) { - this.router.navigateByUrl("/login"); + } else if (unauthenticatedRoutes.indexOf(this.location.path()) == -1) { + this.router.navigateByUrl(`/login?redirect=${this.location.path()}`); } - updates.available.subscribe( + this.updates.available.subscribe( event => { console.log('current version is', event.current); console.log('available version is', event.available); // TODO: Prompt user to click something to update - updates.activateUpdate(); + this.updates.activateUpdate(); }, err => { } ); - updates.activated.subscribe( + this.updates.activated.subscribe( event => { console.log('old version was', event.previous); console.log('new version is', event.current); @@ -70,10 +78,10 @@ export class AppComponent { } ); - const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true)); + const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true)); const everySixHours$ = interval(6 * 60 * 60 * 1000); const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$); - everySixHoursOnceAppIsStable$.subscribe(() => updates.checkForUpdate()); + everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate()); this.user.subscribe( user => { if (user) { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7f6fc72..064f294 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -46,7 +46,6 @@ import { TWIGS_SERVICE } from './shared/twigs.service'; import { AuthInterceptor } from './shared/auth.interceptor'; import { TwigsHttpService } from './shared/twigs.http.service'; import { TwigsLocalService } from './shared/twigs.local.service'; -import { CookieService } from 'ngx-cookie-service'; import { TransactionListComponent } from './transactions/transaction-list/transaction-list.component'; import { EditCategoryComponent } from './categories/edit-category/edit-category.component'; import { EditBudgetComponent } from './budgets/edit-budget/edit-budget.component'; @@ -114,8 +113,8 @@ export const CustomCurrencyMaskConfig: CurrencyMaskConfig = { { provide: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: TWIGS_SERVICE, useClass: TwigsHttpService }, + { provide: Storage, useValue: window.localStorage }, // { provide: TWIGS_SERVICE, useClass: TwigsLocalService }, - CookieService ], bootstrap: [AppComponent] }) diff --git a/src/app/shared/auth.interceptor.ts b/src/app/shared/auth.interceptor.ts index 21a1a4f..e684fbf 100644 --- a/src/app/shared/auth.interceptor.ts +++ b/src/app/shared/auth.interceptor.ts @@ -1,22 +1,21 @@ import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { CookieService } from 'ngx-cookie-service'; import { Observable } from 'rxjs'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor( - private cookieService: CookieService + private storage: Storage ) { } intercept(req: HttpRequest, next: HttpHandler): Observable> { - if (!this.cookieService.check('Authorization')) { + let token = this.storage.getItem('Authorization') + if (!token) { return next.handle(req); } let headers = req.headers; - headers = headers.append('Authorization', `Basic ${this.cookieService.get('Authorization')}`); - this.cookieService.set('Authorization', this.cookieService.get('Authorization'), 14, null, null, true); + headers = headers.append('Authorization', `Bearer ${token}`); return next.handle(req.clone({headers: headers})); } } \ No newline at end of file diff --git a/src/app/shared/twigs.http.service.ts b/src/app/shared/twigs.http.service.ts index d969c4e..d01685d 100644 --- a/src/app/shared/twigs.http.service.ts +++ b/src/app/shared/twigs.http.service.ts @@ -1,42 +1,46 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; import { BehaviorSubject, Observable, pipe, Subscriber } from 'rxjs'; -import { User, UserPermission, Permission } from '../users/user'; +import { User, UserPermission, Permission, AuthToken } from '../users/user'; import { TwigsService } from './twigs.service'; import { Budget } from '../budgets/budget'; import { Category } from '../categories/category'; import { Transaction } from '../transactions/transaction'; import { environment } from '../../environments/environment'; import { map } from 'rxjs/operators'; -import { CookieService } from 'ngx-cookie-service'; @Injectable({ providedIn: 'root' }) export class TwigsHttpService implements TwigsService { - constructor( - private http: HttpClient, - private cookieService: CookieService - ) { } - private options = { withCredentials: true }; - private apiUrl = environment.apiUrl; - private budgets: BehaviorSubject = new BehaviorSubject(null); - // Auth + + constructor( + private http: HttpClient, + private storage: Storage + ) { } + login(email: string, password: string): Observable { - // const params = { - // 'username': email, - // 'password': password - // }; - // return this.http.post(this.apiUrl + '/users/login', params, this.options); - const credentials = btoa(`${email}:${password}`) - this.cookieService.set('Authorization', credentials, 14, null, null, true); - return this.getProfile(); + return new Observable(emitter => { + const params = { + 'username': email, + 'password': password + }; + this.http.post(this.apiUrl + '/users/login', params, this.options) + .subscribe( + auth => { + // TODO: Use token expiration to determine cookie expiration + this.storage.setItem('Authorization', auth.token); + this.getProfile().subscribe(user => emitter.next(user), error => emitter.error(error)); + }, + error => emitter.error(error) + ); + }); } register(username: string, email: string, password: string): Observable { @@ -49,8 +53,8 @@ export class TwigsHttpService implements TwigsService { } logout(): Observable { - return Observable.create(emitter => { - this.cookieService.delete('Authorization'); + return new Observable(emitter => { + this.storage.removeItem('Authorization'); emitter.next(); emitter.complete(); }) @@ -74,7 +78,7 @@ export class TwigsHttpService implements TwigsService { cachedBudget = this.budgets.value.find(budget => { return budget.id === id; }); - } + } if (cachedBudget) { emitter.next(cachedBudget); emitter.complete(); @@ -140,7 +144,7 @@ export class TwigsHttpService implements TwigsService { var index = updatedBudgets.findIndex(oldBudget => oldBudget.id === id); if (index > -1) { updatedBudgets.splice(index, 1); - } + } updatedBudgets.push(budget); updatedBudgets.sort(); this.budgets.next(updatedBudgets); diff --git a/src/app/users/login/login.component.ts b/src/app/users/login/login.component.ts index 33bef40..2863c1a 100644 --- a/src/app/users/login/login.component.ts +++ b/src/app/users/login/login.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, Inject, ChangeDetectorRef } from '@angula import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service'; import { User } from '../user'; import { AppComponent } from 'src/app/app.component'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'app-login', @@ -14,16 +14,19 @@ export class LoginComponent implements OnInit { public isLoading = false; public email: string; public password: string; + private redirect: string; constructor( private app: AppComponent, @Inject(TWIGS_SERVICE) private twigsService: TwigsService, private router: Router, + private activatedRoute: ActivatedRoute ) { } ngOnInit() { this.app.setTitle('Login') this.app.setBackEnabled(true); + this.redirect = this.activatedRoute.snapshot.queryParamMap.get('redirect'); } login(): void { @@ -31,10 +34,11 @@ export class LoginComponent implements OnInit { this.twigsService.login(this.email, this.password) .subscribe(user => { this.app.user.next(user); - this.router.navigate(['/']) + this.router.navigate([this.redirect || '/']) }, error => { console.error(error) + //TODO: Replace this with an in-app dialog alert("Login failed. Please verify you have the correct credentials"); this.isLoading = false; }) diff --git a/src/app/users/user.ts b/src/app/users/user.ts index 7ee7e66..6fa45f3 100644 --- a/src/app/users/user.ts +++ b/src/app/users/user.ts @@ -12,6 +12,11 @@ export class User { } } +export class AuthToken { + token: string; + expiration: Date; +} + export class UserPermission { user: User; permission: Permission; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 7558363..95ad40a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -13,4 +13,4 @@ export const environment = { * below file. Don't forget to comment it out in production mode * because it will have a performance impact when errors are thrown */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. +import 'zone.js/dist/zone-error'; // Included with Angular CLI.