diff --git a/setup-wizard/src/app/app.module.ts b/setup-wizard/src/app/app.module.ts index cb0216db3..7f8e16b4f 100644 --- a/setup-wizard/src/app/app.module.ts +++ b/setup-wizard/src/app/app.module.ts @@ -4,6 +4,8 @@ import { RouteReuseStrategy } from '@angular/router'; import { HttpClientModule } from '@angular/common/http'; import { ApiService } from './services/api/api.service' import { MockApiService } from './services/api/mock-api.service' +import { LiveApiService } from './services/api/live-api.service' +import { HttpService } from './services/api/http.service' import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import * as config from './config/config' @@ -23,9 +25,14 @@ import { AppRoutingModule } from './app-routing.module'; { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: ApiService , - useFactory: () => { - return new MockApiService() + useFactory: (http: HttpService) => { + if(config.config.useMocks) { + return new MockApiService() + } else { + return new LiveApiService(http) + } }, + deps: [HttpService] }, ], bootstrap: [AppComponent], diff --git a/setup-wizard/src/app/services/api/http.service.ts b/setup-wizard/src/app/services/api/http.service.ts new file mode 100644 index 000000000..5998200a8 --- /dev/null +++ b/setup-wizard/src/app/services/api/http.service.ts @@ -0,0 +1,178 @@ +import { Injectable } from '@angular/core' +import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http' +import { Observable, from, interval, race, Subject } from 'rxjs' +import { map, take } from 'rxjs/operators' + +@Injectable({ + providedIn: 'root', +}) +export class HttpService { + private unauthorizedApiResponse$ = new Subject() + fullUrl: string + + constructor ( + private readonly http: HttpClient, + ) { + const port = window.location.port + this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}` + } + + watchUnauth$ (): Observable<{ }> { + return this.unauthorizedApiResponse$.asObservable() + } + + async rpcRequest (rpcOpts: RPCOptions): Promise { + rpcOpts.params = rpcOpts.params || { } + const httpOpts = { + method: Method.POST, + body: rpcOpts, + url: `this.fullUrl`, + } + + const res = await this.httpRequest>(httpOpts) + + if (isRpcError(res)) { + if (res.error.code === 34) this.unauthorizedApiResponse$.next(true) + throw new RpcError(res.error) + } + + if (isRpcSuccess(res)) return res.result + } + + async httpRequest (httpOpts: HttpOptions): Promise { + if (httpOpts.withCredentials !== false) { + httpOpts.withCredentials = true + } + + const urlIsRelative = httpOpts.url.startsWith('/') + const url = urlIsRelative ? + this.fullUrl + httpOpts.url : + httpOpts.url + + Object.keys(httpOpts.params || { }).forEach(key => { + if (httpOpts.params[key] === undefined) { + delete httpOpts.params[key] + } + }) + + const options = { + responseType: httpOpts.responseType || 'json', + body: httpOpts.body, + observe: 'events', + reportProgress: false, + withCredentials: httpOpts.withCredentials, + headers: httpOpts.headers, + params: httpOpts.params, + timeout: httpOpts.timeout, + } as any + + let req: Observable<{ body: T }> + switch (httpOpts.method) { + case Method.GET: req = this.http.get(url, options) as any; break + case Method.POST: req = this.http.post(url, httpOpts.body, options) as any; break + case Method.PUT: req = this.http.put(url, httpOpts.body, options) as any; break + case Method.PATCH: req = this.http.patch(url, httpOpts.body, options) as any; break + case Method.DELETE: req = this.http.delete(url, options) as any; break + } + + return (httpOpts.timeout ? withTimeout(req, httpOpts.timeout) : req) + .toPromise() + .then(res => res.body) + .catch(e => { throw new HttpError(e) }) + } +} + +function RpcError (e: RPCError['error']): void { + const { code, message, data } = e + + this.code = code + this.message = message + + if (typeof data === 'string') { + this.details = e.data + } else { + this.details = data.details + } +} + +function HttpError (e: HttpErrorResponse): void { + const { status, statusText } = e + + this.code = status + this.message = statusText + this.details = null +} + +function isRpcError (arg: { error: Error } | { result: Result}): arg is { error: Error } { + return !!(arg as any).error +} + +function isRpcSuccess (arg: { error: Error } | { result: Result}): arg is { result: Result } { + return !!(arg as any).result +} + +export enum Method { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE', +} + +export interface RPCOptions { + method: string + // @TODO what are valid params? object, bool? + params?: { + [param: string]: string | number | boolean | object | string[] | number[]; + } +} + +interface RPCBase { + jsonrpc: '2.0' + id: string +} + +export interface RPCRequest extends RPCBase { + method: string + params?: T +} + +export interface RPCSuccess extends RPCBase { + result: T +} + +export interface RPCError extends RPCBase { + error: { + code: number, + message: string + data?: { + details: string + } | string + } +} + +export type RPCResponse = RPCSuccess | RPCError + +type HttpError = HttpErrorResponse & { error: { code: string, message: string } } + +export interface HttpOptions { + method: Method + url: string + headers?: HttpHeaders | { + [header: string]: string | string[] + } + params?: HttpParams | { + [param: string]: string | string[] + } + responseType?: 'json' | 'text' + withCredentials?: boolean + body?: any + timeout?: number +} + +function withTimeout (req: Observable, timeout: number): Observable { + return race( + from(req.toPromise()), // this guarantees it only emits on completion, intermediary emissions are suppressed. + interval(timeout).pipe(take(1), map(() => { throw new Error('timeout') })), + ) +} diff --git a/setup-wizard/src/app/services/api/live-api.service.ts b/setup-wizard/src/app/services/api/live-api.service.ts new file mode 100644 index 000000000..0008344d7 --- /dev/null +++ b/setup-wizard/src/app/services/api/live-api.service.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@angular/core' +import { pauseFor } from '../state.service' +import { ApiService } from './api.service' +import { HttpService } from './http.service' + +@Injectable({ + providedIn: 'root' +}) +export class LiveApiService extends ApiService { + + constructor( + private readonly http: HttpService + ) { super() } + + async verifyProductKey(key) { + await pauseFor(2000) + return { + "is-recovering": false, + "tor-address": null + } + } + + async getDataTransferProgress() { + tries = Math.min(tries + 1, 4) + return { + 'bytes-transfered': tries, + 'total-bytes': 4 + } + } + + async getEmbassyDrives() { + return [ + { + logicalname: 'Name1', + labels: ['label 1', 'label 2'], + capacity: 1600.66666, + used: 200.1255312, + }, + { + logicalname: 'Name2', + labels: [], + capacity: 1600.01234, + used: 0.00, + } + ] + } + + async getRecoveryDrives() { + await pauseFor(2000) + return [ + { + logicalname: 'Name1', + version: '0.3.3', + name: 'My Embassy' + }, + { + logicalname: 'Name2', + version: '0.2.7', + name: 'My Embassy' + } + ] + } + + async verifyRecoveryPassword(logicalname, password) { + await pauseFor(2000) + return password.length > 8 + } + + async setupEmbassy (setupInfo) { + await pauseFor(2000) + return { "tor-address": 'asdfasdfasdf.onion' } + } +} + +let tries = 0