You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
116 lines
4.0 KiB
116 lines
4.0 KiB
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<HttpApiEvents> { |
|
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<any> { |
|
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"); |
|
} |
|
}
|
|
|