diff --git a/README.md b/README.md index 8fb2c78..2d203d4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ Run `ng generate module module-name --route module-name --module app.module` to ## Build +set you environment variables - set these via environment variables as found in set-env.ts +// TODO create a .env file so people don't have to set these one-by-one + Run `npm run build:dev` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `build:prod` script for a production build. ## Running unit tests diff --git a/src/app/_eth/registry.ts b/src/app/_eth/registry.ts index 451506e..3ee136a 100644 --- a/src/app/_eth/registry.ts +++ b/src/app/_eth/registry.ts @@ -1,6 +1,7 @@ // @ts-ignore import * as registry from '@src/assets/js/block-sync/data/Registry.json'; import {environment} from '@src/environments/environment'; +import {LoggingService} from '@app/_services/logging.service'; const Web3 = require('web3'); const web3 = new Web3(environment.web3Provider); @@ -27,6 +28,12 @@ export class Registry { public async addressOf(identifier: string): Promise { const id = '0x' + web3.utils.padRight(new Buffer(identifier).toString('hex'), 64); - return await this.contract.methods.addressOf(id).call(); + try { + return await this.contract.methods.addressOf(id).call(); + } catch (error) { + // TODO logger service + // this.loggingService.sendInfoLevelMessage + console.log('Unable to fetch addressOf', error) + } } } diff --git a/src/app/_guards/auth.guard.ts b/src/app/_guards/auth.guard.ts index 2aed323..d0af8b9 100644 --- a/src/app/_guards/auth.guard.ts +++ b/src/app/_guards/auth.guard.ts @@ -12,10 +12,9 @@ export class AuthGuard implements CanActivate { canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) { + if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) { return true; } - this.router.navigate(['/auth']); return false; } diff --git a/src/app/_helpers/global-error-handler.ts b/src/app/_helpers/global-error-handler.ts index e524bd0..52d808e 100644 --- a/src/app/_helpers/global-error-handler.ts +++ b/src/app/_helpers/global-error-handler.ts @@ -3,6 +3,16 @@ import {LoggingService} from '@app/_services/logging.service'; import {HttpErrorResponse} from '@angular/common/http'; import {Router} from '@angular/router'; +// A generalized http repsonse error +export class HttpError extends Error { + public status: number + constructor(message, status) { + super(message); + this.status = status; + this.name = 'HttpError'; + } +} + @Injectable() export class GlobalErrorHandler extends ErrorHandler { private sentencesForWarningLogging: string[] = []; @@ -14,13 +24,13 @@ export class GlobalErrorHandler extends ErrorHandler { super(); } - handleError(error: any): void { + handleError(error: Error): void { this.logError(error); const message = error.message ? error.message : error.toString(); - if (error.status) { - error = new Error(message); - } + // if (error.status) { + // error = new Error(message); + // } const errorTraceString = `Error message:\n${message}.\nStack trace: ${error.stack}`; diff --git a/src/app/_interceptors/error.interceptor.ts b/src/app/_interceptors/error.interceptor.ts index 9763c8d..69819fd 100644 --- a/src/app/_interceptors/error.interceptor.ts +++ b/src/app/_interceptors/error.interceptor.ts @@ -20,29 +20,30 @@ export class ErrorInterceptor implements HttpInterceptor { ) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { - return next.handle(request).pipe( - catchError((err: HttpErrorResponse) => { - let errorMessage; - if (err.error instanceof ErrorEvent) { - // A client-side or network error occurred. Handle it accordingly. - errorMessage = `An error occurred: ${err.error.message}`; - } else { - // The backend returned an unsuccessful response code. - // The response body may contain clues as to what went wrong. - errorMessage = `Backend returned code ${err.status}, body was: ${JSON.stringify(err.error)}`; - } - this.loggingService.sendErrorLevelMessage(errorMessage, this, {error: err}); - switch (err.status) { - case 401: // unauthorized - this.router.navigateByUrl('/auth').then(); - break; - case 403: // forbidden - location.reload(true); - break; - } - // Return an observable with a user-facing error message. - return throwError(err); - }) - ); + return next.handle(request).pipe( + catchError((err: HttpErrorResponse) => { + let errorMessage; + if (err.error instanceof ErrorEvent) { + // A client-side or network error occurred. Handle it accordingly. + errorMessage = `An error occurred: ${err.error.message}`; + } else { + // The backend returned an unsuccessful response code. + // The response body may contain clues as to what went wrong. + errorMessage = `Backend returned code ${err.status}, body was: ${JSON.stringify(err.error)}`; + } + this.loggingService.sendErrorLevelMessage(errorMessage, this, {error: err}); + switch (err.status) { + case 401: // unauthorized + this.router.navigateByUrl('/auth').then(); + break; + case 403: // forbidden + location.reload(true); + break; + } + // Return an observable with a user-facing error message. + return throwError(err); + }) + ); + return next.handle(request); } } diff --git a/src/app/_interceptors/logging.interceptor.ts b/src/app/_interceptors/logging.interceptor.ts index 4858a5b..d992e73 100644 --- a/src/app/_interceptors/logging.interceptor.ts +++ b/src/app/_interceptors/logging.interceptor.ts @@ -18,20 +18,21 @@ export class LoggingInterceptor implements HttpInterceptor { ) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { - this.loggingService.sendInfoLevelMessage(request); - const startTime = Date.now(); - let status: string; + return next.handle(request); + // this.loggingService.sendInfoLevelMessage(request); + // const startTime = Date.now(); + // let status: string; - return next.handle(request).pipe(tap(event => { - status = ''; - if (event instanceof HttpResponse) { - status = 'succeeded'; - } - }, error => status = 'failed'), - finalize(() => { - const elapsedTime = Date.now() - startTime; - const message = `${request.method} request for ${request.urlWithParams} ${status} in ${elapsedTime} ms`; - this.loggingService.sendInfoLevelMessage(message); - })); + // return next.handle(request).pipe(tap(event => { + // status = ''; + // if (event instanceof HttpResponse) { + // status = 'succeeded'; + // } + // }, error => status = 'failed'), + // finalize(() => { + // const elapsedTime = Date.now() - startTime; + // const message = `${request.method} request for ${request.urlWithParams} ${status} in ${elapsedTime} ms`; + // this.loggingService.sendInfoLevelMessage(message); + // })); } } diff --git a/src/app/_services/auth.service.ts b/src/app/_services/auth.service.ts index ba4651a..731602a 100644 --- a/src/app/_services/auth.service.ts +++ b/src/app/_services/auth.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@angular/core'; import { hobaParseChallengeHeader } from '@src/assets/js/hoba.js'; import { signChallenge } from '@src/assets/js/hoba-pgp.js'; -import {environment} from '@src/environments/environment'; -import {LoggingService} from '@app/_services/logging.service'; -import {MutableKeyStore, MutablePgpKeyStore} from '@app/_pgp'; -import {ErrorDialogService} from '@app/_services/error-dialog.service'; +import { environment } from '@src/environments/environment'; +import { LoggingService } from '@app/_services/logging.service'; +import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp'; +import { ErrorDialogService } from '@app/_services/error-dialog.service'; import { HttpClient } from '@angular/common/http'; -import {Observable } from 'rxjs'; +import { Observable } from 'rxjs'; +import { HttpError } from '@app/_helpers/global-error-handler'; @Injectable({ providedIn: 'root' @@ -14,20 +15,25 @@ import {Observable } from 'rxjs'; export class AuthService { sessionToken: any; sessionLoginCount = 0; - privateKey: any; - mutableKeyStore: MutableKeyStore = new MutablePgpKeyStore(); + mutableKeyStore: MutableKeyStore; constructor( private httpClient: HttpClient, private loggingService: LoggingService, private errorDialogService: ErrorDialogService ) { + this.mutableKeyStore = new MutablePgpKeyStore() + } + + async init(): Promise { + this.mutableKeyStore.loadKeyring(); // TODO setting these together shoulds be atomic if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) { this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); } if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) { - this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY')); + this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) + // this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY')); } } @@ -53,24 +59,28 @@ export class AuthService { xhr.send(); } - sendResponse(hobaResponseEncoded): void { - const xhr = new XMLHttpRequest(); - xhr.responseType = 'text'; - xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1)); - xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded); - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('x-cic-automerge', 'none'); - xhr.addEventListener('load', (e) => { - if (xhr.status === 401) { - throw new Error('login rejected'); - } - this.sessionToken = xhr.getResponseHeader('Token'); - sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken); - this.sessionLoginCount++; - this.setState('Click button to log in'); - return; - }); - xhr.send(); + //TODO renmae to send signed challenge and set session. Also seperate these responsibilities + sendResponse(hobaResponseEncoded): Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.responseType = 'text'; + xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1)); + xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('x-cic-automerge', 'none'); + xhr.addEventListener('load', (e) => { + if (xhr.status !== 200) { + const error = new HttpError(xhr.statusText, xhr.status); + return reject(error); + } + this.sessionToken = xhr.getResponseHeader('Token'); + sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken); + this.sessionLoginCount++; + this.setState('Click button to log in'); + return resolve(true); + }); + xhr.send(); + }) } getChallenge(): void { @@ -81,39 +91,38 @@ export class AuthService { if (xhr.status === 401) { const authHeader = xhr.getResponseHeader('WWW-Authenticate'); const o = hobaParseChallengeHeader(authHeader); - await this.loginResponse(o); + this.loginResponse(o); } }; xhr.send(); } - - login(): boolean { - if (this.sessionToken !== undefined) { - try { - this.getWithToken(); - return true; - } catch (e) { - this.loggingService.sendErrorLevelMessage('Login token failed', this, {error: e}); - } - } else { - try { - this.getChallenge(); - } catch (e) { - this.loggingService.sendErrorLevelMessage('Login challenge failed', this, {error: e}); - } - } - return false; - } - - async loginResponse(o): Promise { - try { - const r = await signChallenge(o.challenge, o.realm, environment.cicMetaUrl, this.mutableKeyStore); - this.sendResponse(r); - } catch (error) { - this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'}); - } + return new Promise(async(resolve, reject) => { + try { + const r = await signChallenge(o.challenge, + o.realm, + environment.cicMetaUrl, + this.mutableKeyStore); + const sessionTokenResult = await this.sendResponse(r); + } catch (error) { + if (error instanceof HttpError) { + if (error.status === 403) { + this.errorDialogService.openDialog({ message: 'You are not authorized to use this system' }) + } + if (error.status === 401) { + this.errorDialogService.openDialog({ message: 'Unable to authenticate with the service. ' + + 'Please speak with the staff at Grassroots ' + + 'Economics for requesting access ' + + 'staff@grassrootseconomics.net.' }) + + } + } + // TODO define this error + this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'}); + resolve(false); + } + }); } loginView(): void { @@ -128,10 +137,11 @@ export class AuthService { if (!isValidKeyCheck) { throw Error('The private key is invalid'); } - const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored); - if (!isEncryptedKeyCheck) { - throw Error('The private key doesn\'t have a password!'); - } + // TODO leaving this out for now. + //const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored); + //if (!isEncryptedKeyCheck) { + // throw Error('The private key doesn\'t have a password!'); + //} const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored); localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored); } catch (err) { @@ -157,13 +167,19 @@ export class AuthService { return trustedUsers; } - getPublicKeys(): Observable { - return this.httpClient.get(`${environment.publicKeysUrl}`, {responseType: 'text'}); + async getPublicKeys(): Promise { + const data = await fetch(environment.publicKeysUrl) + .then(res => { + if (!res.ok) { + //TODO does angular recommend an error interface? + throw Error(`${res.statusText} - ${res.status}`); + } + return res.text(); + }) + return data; } - async getPrivateKeys(): Promise { - if (this.privateKey !== undefined) { - await this.mutableKeyStore.importPrivateKey(this.privateKey); - } + getPrivateKey(): any { + return this.mutableKeyStore.getPrivateKey(); } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ce9e6f0..2cc5c1b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -21,12 +21,20 @@ export class AppComponent { private errorDialogService: ErrorDialogService ) { (async () => { - await this.authService.mutableKeyStore.loadKeyring(); - this.authService.getPublicKeys() - .pipe(catchError(async (error) => { - this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error}); - this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'}); - })).subscribe(this.authService.mutableKeyStore.importPublicKey); + try { + await this.authService.mutableKeyStore.loadKeyring(); + // this.authService.getPublicKeys() + // .pipe(catchError(async (error) => { + // this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error}); + // this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'}); + // })).subscribe(this.authService.mutableKeyStore.importPublicKey); + const publicKeys = await this.authService.getPublicKeys() + await this.authService.mutableKeyStore.importPublicKey(publicKeys); + } catch(error) { + this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'}); + // TODO do something to halt user progress...show a sad cicada page 🦗? + } + })(); this.mediaQuery.addListener(this.onResize); this.onResize(this.mediaQuery); diff --git a/src/app/auth/auth.component.ts b/src/app/auth/auth.component.ts index 5f7264a..19419db 100644 --- a/src/app/auth/auth.component.ts +++ b/src/app/auth/auth.component.ts @@ -26,12 +26,11 @@ export class AuthComponent implements OnInit { this.keyForm = this.formBuilder.group({ key: ['', Validators.required], }); - if (this.authService.privateKey !== undefined) { - const setKey = await this.authService.setKey(this.authService.privateKey); - if (setKey && this.authService.sessionToken !== undefined) { - this.authService.setState('Click button to log in'); - } - } + await this.authService.init(); + //if (this.authService.privateKey !== undefined) { + // const setKey = await this.authService.setKey(this.authService.privateKey); + // } + //} } get keyFormStub(): any { return this.keyForm.controls; } @@ -47,8 +46,12 @@ export class AuthComponent implements OnInit { } login(): void { - const loginStatus = this.authService.login(); - if (loginStatus) { + // TODO check if we have privatekey + // Send us to home if we have a private key + // talk to meta somehow + // in the error interceptor if 401/403 handle it + // if 200 go /home + if (this.authService.getPrivateKey()) { this.router.navigate(['/home']); } } diff --git a/src/app/pages/accounts/accounts.component.ts b/src/app/pages/accounts/accounts.component.ts index 007152c..1b112f0 100644 --- a/src/app/pages/accounts/accounts.component.ts +++ b/src/app/pages/accounts/accounts.component.ts @@ -27,9 +27,11 @@ export class AccountsComponent implements OnInit { private userService: UserService, private loggingService: LoggingService, private router: Router - ) { + ) + { (async () => { try { + // TODO it feels like this shuold be in the onInit handler await this.userService.loadAccounts(100); } catch (error) { this.loggingService.sendErrorLevelMessage('Failed to load accounts', this, {error});