import { action } from "mobx"; import { TokenStore } from "@client/state/TokenStore"; import ApiError from "@common/ApiError"; import { ErrorCode } from "@common/ErrorCode"; import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/httpApi"; import log from "@common/logger"; import { DefaultEvents, TypedEventEmitter } from "@common/TypedEventEmitter"; export { ApiError }; interface HttpApiEvents extends DefaultEvents { tokenGranted(response: TokenGrantResponse): void; error(err: ApiError): void; tokenError(err: ApiError): void; } export default class HttpApi extends TypedEventEmitter { baseUrl: string; tokenStore: TokenStore; private get authorizationHeader(): {} | { Authorization: string } { if (!this.tokenStore.accessToken) { return {}; } return { Authorization: `Bearer ${this.tokenStore.accessToken.token}` }; } constructor( baseUrl: string = `${location.protocol}//${location.hostname}:${ location.port }/api` ) { super(); while (baseUrl.charAt(baseUrl.length - 1) === "/") { baseUrl = baseUrl.substring(0, baseUrl.length - 1); } this.baseUrl = baseUrl; this.tokenStore = new TokenStore(); this.on("error", (err: ApiError) => { if (err.code === ErrorCode.BadToken) { this.emit("tokenError", err); } }); this.on("tokenGranted", this.onTokenGranted); } async makeRequest( url: string, options?: RequestInit, body?: any ): Promise { try { options = options || {}; options = { headers: { "Content-Type": "application/json", ...this.authorizationHeader, ...(options.headers || {}) }, body: JSON.stringify(body), ...options }; let response: Response; try { response = await fetch(this.baseUrl + url, options); } catch (err) { throw new ApiError("Http request error", ErrorCode.Internal, err); } let responseBody: any; try { responseBody = (await response.json()) || {}; } catch (e) { throw new ApiError("Invalid JSON response", ErrorCode.Internal, e); } if (!response.ok) { throw new ApiError( responseBody.message || response.statusText, responseBody.code, responseBody.data ); } return responseBody; } catch (err) { this.emit("error", err); throw err; } } async grantPassword(username: string, password: string) { const request: TokenGrantPasswordRequest = { grant_type: "password", username, password }; const response: TokenGrantResponse = await this.makeRequest( "/token/grant", { method: "POST" }, request ); this.emit("tokenGranted", response); } async grantRefresh() { const { refreshToken } = this.tokenStore; if (!refreshToken.isValid) { throw new ApiError("can not grant refresh with invalid refresh_token"); } const request: TokenGrantRefreshRequest = { grant_type: "refresh", refresh_token: refreshToken.token! }; const response: TokenGrantResponse = await this.makeRequest( "/token/grant", { method: "POST" }, request ); this.emit("tokenGranted", response); } @action.bound private onTokenGranted(response: TokenGrantResponse) { this.tokenStore.accessToken.token = response.access_token; this.tokenStore.refreshToken.token = response.refresh_token; this.tokenStore.saveLocalStorage(); const { accessToken, refreshToken } = this.tokenStore; log.debug( { accessToken: accessToken.claims, refreshToken: refreshToken.claims }, "got new tokens" ); } }