diff options
Diffstat (limited to 'packages/demobank-ui/src/hooks/useCredentialsChecker.ts')
-rw-r--r-- | packages/demobank-ui/src/hooks/useCredentialsChecker.ts | 135 |
1 files changed, 135 insertions, 0 deletions
diff --git a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts new file mode 100644 index 000000000..b3dedb654 --- /dev/null +++ b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts @@ -0,0 +1,135 @@ +import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; +import { ErrorType, HttpError, RequestError, useApiContext } from "@gnu-taler/web-util/browser"; +import { getInitialBackendBaseURL } from "./backend.js"; + +export function useCredentialsChecker() { + const { request } = useApiContext(); + const baseUrl = getInitialBackendBaseURL(); + //check against instance details endpoint + //while merchant backend doesn't have a login endpoint + async function requestNewLoginToken( + username: string, + password: string, + ): Promise<LoginResult> { + const data: LoginTokenRequest = { + scope: "readwrite" as "write", //FIX: different than merchant + duration: { + // d_us: "forever" //FIX: should return shortest + d_us: 60 * 60 * 24 * 7 * 1000 * 1000 + }, + refreshable: true, + } + try { + const response = await request<LoginTokenSuccessResponse>(baseUrl, `accounts/${username}/token`, { + method: "POST", + basicAuth: { + username, + password, + }, + data, + contentType: "json" + }); + return { valid: true, token: `secret-token:${response.data.access_token}` as AccessToken, expiration: response.data.expiration }; + } catch (error) { + if (error instanceof RequestError) { + return { valid: false, cause: error.cause }; + } + + return { + valid: false, cause: { + type: ErrorType.UNEXPECTED, + loading: false, + info: { + hasToken: true, + status: 0, + options: {}, + url: `/private/token`, + payload: {} + }, + exception: error, + message: (error instanceof Error ? error.message : "unpexepected error") + } + }; + } + }; + + async function refreshLoginToken( + baseUrl: string, + token: LoginToken + ): Promise<LoginResult> { + + if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { + return { + valid: false, cause: { + type: ErrorType.CLIENT, + status: HttpStatusCode.Unauthorized, + message: "login token expired, login again.", + info: { + hasToken: true, + status: 401, + options: {}, + url: `/private/token`, + payload: {} + }, + payload: {} + }, + } + } + + return requestNewLoginToken(baseUrl, token.token) + } + return { requestNewLoginToken, refreshLoginToken } +} + +export interface LoginToken { + token: AccessToken, + expiration: Timestamp, +} +// token used to get loginToken +// must forget after used +declare const __ac_token: unique symbol; +export type AccessToken = string & { + [__ac_token]: true; +}; + +type YesOrNo = "yes" | "no"; +export type LoginResult = { + valid: true; + token: AccessToken; + expiration: Timestamp; +} | { + valid: false; + cause: HttpError<{}>; +} + + +// DELETE /private/instances/$INSTANCE +export interface LoginTokenRequest { + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + duration?: RelativeTime; + + // Can this token be refreshed? + // Defaults to false. + refreshable?: boolean; +} +export interface LoginTokenSuccessResponse { + // The login token that can be used to access resources + // that are in scope for some time. Must be prefixed + // with "Bearer " when used in the "Authorization" HTTP header. + // Will already begin with the RFC 8959 prefix. + access_token: AccessToken; + + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + expiration: Timestamp; + + // Can this token be refreshed? + refreshable: boolean; +} |