Use bearer instead of basic auth

This commit is contained in:
William Brawner 2021-01-26 21:00:15 -07:00
parent 84dae70b7f
commit ae14a33616
10 changed files with 75 additions and 55 deletions

7
.vscode/launch.json vendored
View file

@ -4,6 +4,13 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Launch Chrome",
"request": "launch",
"type": "pwa-chrome",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}"
},
{ {
"name": "ng serve", "name": "ng serve",
"type": "node", "type": "node",

5
package-lock.json generated
View file

@ -8470,11 +8470,6 @@
"resolved": "https://registry.npmjs.org/ng2-currency-mask/-/ng2-currency-mask-9.0.2.tgz", "resolved": "https://registry.npmjs.org/ng2-currency-mask/-/ng2-currency-mask-9.0.2.tgz",
"integrity": "sha512-ydtSTEqXR32mHsCElDvmSEywmN45Dam4aWUhoYJl3eEm9T6QWgA3DzHxNiFcur2kH94gomdvlQAB490CcObGMg==" "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": { "nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",

View file

@ -30,7 +30,6 @@
"dexie": "^2.0.4", "dexie": "^2.0.4",
"ng2-charts": "^2.4.2", "ng2-charts": "^2.4.2",
"ng2-currency-mask": "^9.0.2", "ng2-currency-mask": "^9.0.2",
"ngx-cookie-service": "^2.4.0",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"zone.js": "~0.10.3" "zone.js": "~0.10.3"

View file

@ -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 { Location } from '@angular/common';
import { User } from './users/user'; import { User } from './users/user';
import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service'; import { TWIGS_SERVICE, TwigsService } from './shared/twigs.service';
import { CookieService } from 'ngx-cookie-service';
import { SwUpdate } from '@angular/service-worker'; import { SwUpdate } from '@angular/service-worker';
import { first, filter, map } from 'rxjs/operators'; import { first, filter, map } from 'rxjs/operators';
import { interval, concat, BehaviorSubject } from 'rxjs'; import { interval, concat, BehaviorSubject } from 'rxjs';
@ -14,7 +13,7 @@ import { Actionable, isActionable } from './shared/actionable';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.css'] styleUrls: ['./app.component.css']
}) })
export class AppComponent { export class AppComponent implements OnInit {
public title = 'Twigs'; public title = 'Twigs';
public backEnabled = false; public backEnabled = false;
public user = new BehaviorSubject<User>(null); public user = new BehaviorSubject<User>(null);
@ -26,41 +25,50 @@ export class AppComponent {
constructor( constructor(
@Inject(TWIGS_SERVICE) private twigsService: TwigsService, @Inject(TWIGS_SERVICE) private twigsService: TwigsService,
private location: Location, private location: Location,
private cookieService: CookieService,
private router: Router, private router: Router,
private activatedRoute: ActivatedRoute,
private appRef: ApplicationRef, private appRef: ApplicationRef,
private updates: SwUpdate, private updates: SwUpdate,
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
) { private storage: Storage
){}
ngOnInit(): void {
const unauthenticatedRoutes = [ const unauthenticatedRoutes = [
'',
'/', '/',
'/login', '/login',
'/register' '/register'
] ]
if (this.cookieService.check('Authorization')) { let auth = this.storage.getItem('Authorization');
this.twigsService.getProfile().subscribe(user => { let savedUser = JSON.parse(this.storage.getItem('user')) as User;
this.user.next(user); if (auth && auth.length == 255) {
if (this.router.url == '/') { 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"); this.router.navigateByUrl("/budgets");
} }
}); });
} else if (unauthenticatedRoutes.indexOf(this.router.url) == -1) { } else if (unauthenticatedRoutes.indexOf(this.location.path()) == -1) {
this.router.navigateByUrl("/login"); this.router.navigateByUrl(`/login?redirect=${this.location.path()}`);
} }
updates.available.subscribe( this.updates.available.subscribe(
event => { event => {
console.log('current version is', event.current); console.log('current version is', event.current);
console.log('available version is', event.available); console.log('available version is', event.available);
// TODO: Prompt user to click something to update // TODO: Prompt user to click something to update
updates.activateUpdate(); this.updates.activateUpdate();
}, },
err => { err => {
} }
); );
updates.activated.subscribe( this.updates.activated.subscribe(
event => { event => {
console.log('old version was', event.previous); console.log('old version was', event.previous);
console.log('new version is', event.current); 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 everySixHours$ = interval(6 * 60 * 60 * 1000);
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$); const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
everySixHoursOnceAppIsStable$.subscribe(() => updates.checkForUpdate()); everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate());
this.user.subscribe( this.user.subscribe(
user => { user => {
if (user) { if (user) {

View file

@ -46,7 +46,6 @@ import { TWIGS_SERVICE } from './shared/twigs.service';
import { AuthInterceptor } from './shared/auth.interceptor'; import { AuthInterceptor } from './shared/auth.interceptor';
import { TwigsHttpService } from './shared/twigs.http.service'; import { TwigsHttpService } from './shared/twigs.http.service';
import { TwigsLocalService } from './shared/twigs.local.service'; import { TwigsLocalService } from './shared/twigs.local.service';
import { CookieService } from 'ngx-cookie-service';
import { TransactionListComponent } from './transactions/transaction-list/transaction-list.component'; import { TransactionListComponent } from './transactions/transaction-list/transaction-list.component';
import { EditCategoryComponent } from './categories/edit-category/edit-category.component'; import { EditCategoryComponent } from './categories/edit-category/edit-category.component';
import { EditBudgetComponent } from './budgets/edit-budget/edit-budget.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: CURRENCY_MASK_CONFIG, useValue: CustomCurrencyMaskConfig },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: TWIGS_SERVICE, useClass: TwigsHttpService }, { provide: TWIGS_SERVICE, useClass: TwigsHttpService },
{ provide: Storage, useValue: window.localStorage },
// { provide: TWIGS_SERVICE, useClass: TwigsLocalService }, // { provide: TWIGS_SERVICE, useClass: TwigsLocalService },
CookieService
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View file

@ -1,22 +1,21 @@
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders } from '@angular/common/http'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CookieService } from 'ngx-cookie-service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
constructor( constructor(
private cookieService: CookieService private storage: Storage
) { } ) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.cookieService.check('Authorization')) { let token = this.storage.getItem('Authorization')
if (!token) {
return next.handle(req); return next.handle(req);
} }
let headers = req.headers; let headers = req.headers;
headers = headers.append('Authorization', `Basic ${this.cookieService.get('Authorization')}`); headers = headers.append('Authorization', `Bearer ${token}`);
this.cookieService.set('Authorization', this.cookieService.get('Authorization'), 14, null, null, true);
return next.handle(req.clone({headers: headers})); return next.handle(req.clone({headers: headers}));
} }
} }

View file

@ -1,42 +1,46 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { BehaviorSubject, Observable, pipe, Subscriber } from 'rxjs'; 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 { TwigsService } from './twigs.service';
import { Budget } from '../budgets/budget'; import { Budget } from '../budgets/budget';
import { Category } from '../categories/category'; import { Category } from '../categories/category';
import { Transaction } from '../transactions/transaction'; import { Transaction } from '../transactions/transaction';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { CookieService } from 'ngx-cookie-service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TwigsHttpService implements TwigsService { export class TwigsHttpService implements TwigsService {
constructor(
private http: HttpClient,
private cookieService: CookieService
) { }
private options = { private options = {
withCredentials: true withCredentials: true
}; };
private apiUrl = environment.apiUrl; private apiUrl = environment.apiUrl;
private budgets: BehaviorSubject<Budget[]> = new BehaviorSubject(null); private budgets: BehaviorSubject<Budget[]> = new BehaviorSubject(null);
// Auth
constructor(
private http: HttpClient,
private storage: Storage
) { }
login(email: string, password: string): Observable<User> { login(email: string, password: string): Observable<User> {
// const params = { return new Observable(emitter => {
// 'username': email, const params = {
// 'password': password 'username': email,
// }; 'password': password
// return this.http.post<User>(this.apiUrl + '/users/login', params, this.options); };
const credentials = btoa(`${email}:${password}`) this.http.post<AuthToken>(this.apiUrl + '/users/login', params, this.options)
this.cookieService.set('Authorization', credentials, 14, null, null, true); .subscribe(
return this.getProfile(); 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<User> { register(username: string, email: string, password: string): Observable<User> {
@ -49,8 +53,8 @@ export class TwigsHttpService implements TwigsService {
} }
logout(): Observable<void> { logout(): Observable<void> {
return Observable.create(emitter => { return new Observable(emitter => {
this.cookieService.delete('Authorization'); this.storage.removeItem('Authorization');
emitter.next(); emitter.next();
emitter.complete(); emitter.complete();
}) })
@ -74,7 +78,7 @@ export class TwigsHttpService implements TwigsService {
cachedBudget = this.budgets.value.find(budget => { cachedBudget = this.budgets.value.find(budget => {
return budget.id === id; return budget.id === id;
}); });
} }
if (cachedBudget) { if (cachedBudget) {
emitter.next(cachedBudget); emitter.next(cachedBudget);
emitter.complete(); emitter.complete();
@ -140,7 +144,7 @@ export class TwigsHttpService implements TwigsService {
var index = updatedBudgets.findIndex(oldBudget => oldBudget.id === id); var index = updatedBudgets.findIndex(oldBudget => oldBudget.id === id);
if (index > -1) { if (index > -1) {
updatedBudgets.splice(index, 1); updatedBudgets.splice(index, 1);
} }
updatedBudgets.push(budget); updatedBudgets.push(budget);
updatedBudgets.sort(); updatedBudgets.sort();
this.budgets.next(updatedBudgets); this.budgets.next(updatedBudgets);

View file

@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, Inject, ChangeDetectorRef } from '@angula
import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service'; import { TwigsService, TWIGS_SERVICE } from '../../shared/twigs.service';
import { User } from '../user'; import { User } from '../user';
import { AppComponent } from 'src/app/app.component'; import { AppComponent } from 'src/app/app.component';
import { Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@ -14,16 +14,19 @@ export class LoginComponent implements OnInit {
public isLoading = false; public isLoading = false;
public email: string; public email: string;
public password: string; public password: string;
private redirect: string;
constructor( constructor(
private app: AppComponent, private app: AppComponent,
@Inject(TWIGS_SERVICE) private twigsService: TwigsService, @Inject(TWIGS_SERVICE) private twigsService: TwigsService,
private router: Router, private router: Router,
private activatedRoute: ActivatedRoute
) { } ) { }
ngOnInit() { ngOnInit() {
this.app.setTitle('Login') this.app.setTitle('Login')
this.app.setBackEnabled(true); this.app.setBackEnabled(true);
this.redirect = this.activatedRoute.snapshot.queryParamMap.get('redirect');
} }
login(): void { login(): void {
@ -31,10 +34,11 @@ export class LoginComponent implements OnInit {
this.twigsService.login(this.email, this.password) this.twigsService.login(this.email, this.password)
.subscribe(user => { .subscribe(user => {
this.app.user.next(user); this.app.user.next(user);
this.router.navigate(['/']) this.router.navigate([this.redirect || '/'])
}, },
error => { error => {
console.error(error) console.error(error)
//TODO: Replace this with an in-app dialog
alert("Login failed. Please verify you have the correct credentials"); alert("Login failed. Please verify you have the correct credentials");
this.isLoading = false; this.isLoading = false;
}) })

View file

@ -12,6 +12,11 @@ export class User {
} }
} }
export class AuthToken {
token: string;
expiration: Date;
}
export class UserPermission { export class UserPermission {
user: User; user: User;
permission: Permission; permission: Permission;

View file

@ -13,4 +13,4 @@ export const environment = {
* below file. Don't forget to comment it out in production mode * below file. Don't forget to comment it out in production mode
* because it will have a performance impact when errors are thrown * 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.