From af623f5096138631383719bf737f5ff21660e052 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Sep 2023 13:54:02 -0300 Subject: [PATCH] preparing for the new token api --- packages/demobank-ui/src/hooks/backend.ts | 2 +- .../src/hooks/useCredentialsChecker.ts | 130 ++++++++++++++++++ packages/demobank-ui/src/pages/LoginForm.tsx | 7 +- 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 packages/demobank-ui/src/hooks/useCredentialsChecker.ts diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index c05ab33e9..9d32db4b8 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -255,7 +255,7 @@ interface InvalidationResult { error: unknown; } -export function useCredentialsChecker() { +export function useCredentialsCheckerOld() { const { request } = useApiContext(); const baseUrl = getInitialBackendBaseURL(); //check against account details endpoint diff --git a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts new file mode 100644 index 000000000..b76754ffe --- /dev/null +++ b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts @@ -0,0 +1,130 @@ +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: AccessToken, + ): Promise { + const data: LoginTokenRequest = { + scope: "write", + duration: { + d_us: "forever" + }, + refreshable: true, + } + try { + const response = await request(baseUrl, `accounts/${username}/token`, { + method: "POST", + token: password, + data + }); + return { valid: true, token: response.data.token, 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 { + + 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 as AccessToken) + } + return { requestNewLoginToken, refreshLoginToken } +} + +export interface LoginToken { + token: string, + 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: string; + 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. + token: string; + + // 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; +} diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 46039005a..f6ea0e1d1 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -19,11 +19,12 @@ import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-ut import { Fragment, VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; -import { useCredentialsChecker } from "../hooks/backend.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty } from "../utils.js"; import { USERNAME_REGEX } from "./RegistrationPage.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { AccessToken, useCredentialsChecker } from "../hooks/useCredentialsChecker.js"; +import { useCredentialsCheckerOld } from "../hooks/backend.js"; /** * Collect and submit login data. @@ -33,7 +34,9 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode { const [username, setUsername] = useState(); const [password, setPassword] = useState(); const { i18n } = useTranslationContext(); - const testLogin = useCredentialsChecker(); + // const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker(); + + const testLogin = useCredentialsCheckerOld(); const ref = useRef(null); useEffect(function focusInput() { ref.current?.focus();