diff options
author | Sebastian <sebasjm@gmail.com> | 2024-04-05 17:53:39 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-04-05 17:53:39 -0300 |
commit | cc38998803141c42511e878441a5a8b15a387436 (patch) | |
tree | 8a9279ccd6349aac98e649ca0b7394c0630a805d /packages | |
parent | f5747b394d14f65d9bee342eb30edf47a36d9751 (diff) |
fix #8276
Diffstat (limited to 'packages')
7 files changed, 184 insertions, 139 deletions
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index 1a4bd6708..d5a05e821 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -64,7 +64,7 @@ export function Application(): VNode { de: strings["de"].completeness, }} > - <MerchantApiProvider baseUrl={new URL("/", baseUrl)} frameOnError={OnConfigError} evictors={{ + <MerchantApiProvider baseUrl={new URL("./", baseUrl)} frameOnError={OnConfigError} evictors={{ management: swrCacheEvictor }}> <SWRConfig diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts index 7a5ef33d7..f3349bf83 100644 --- a/packages/merchant-backoffice-ui/src/context/session.ts +++ b/packages/merchant-backoffice-ui/src/context/session.ts @@ -188,7 +188,7 @@ export function useSessionContext(): SessionStateHandler { if (state.impersonate === undefined) { return; } - const newURL = new URL(`/`, state.impersonate.originalBackendUrl); + const newURL = new URL(`./`, state.impersonate.originalBackendUrl); changeBackend(newURL); const nextState: SessionState = { status: "loggedIn", diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx index 255caa375..d05375b6c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -32,6 +32,7 @@ import { Input } from "../../../../components/form/Input.js"; import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { safeConvertURL } from "../update/UpdatePage.js"; type Entity = TalerMerchantApi.AccountAddDetails & { repeatPassword: string }; @@ -42,19 +43,11 @@ interface Props { const accountAuthType = ["none", "basic"]; -function isValidURL(s: string): boolean { - try { - const parsed = new URL("/", s); - return parsed instanceof URL; - } catch (e) { - return false; - } -} - export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({}); + const facadeURL = safeConvertURL(state.credit_facade_url); const errors: FormErrors<Entity> = { payto_uri: !state.payto_uri ? i18n.str`required` : undefined, @@ -74,9 +67,15 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { }), credit_facade_url: !state.credit_facade_url ? undefined - : !isValidURL(state.credit_facade_url) - ? i18n.str`not valid url` - : undefined, + : !facadeURL + ? i18n.str`Invalid url` + : !facadeURL.href.endsWith("/") + ? i18n.str`URL should end with a '/'` + : facadeURL.searchParams.size > 0 + ? i18n.str`URL should not contain params` + : facadeURL.hash + ? i18n.str`URL should not hash param` + : undefined, repeatPassword: !state.credit_facade_credentials ? undefined : state.credit_facade_credentials.type === "basic" && @@ -94,7 +93,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { if (hasErrors) return Promise.reject(); const credit_facade_url = !state.credit_facade_url ? undefined - : new URL("/", state.credit_facade_url).href; + : facadeURL?.href; const credit_facade_credentials: | TalerMerchantApi.FacadeCredentials | undefined = diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx index 96ca8bf5e..fb50ab995 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -62,7 +62,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { onCreate={async (request: Entity) => { const revenueAPI = !request.credit_facade_url ? undefined - : new URL("/", request.credit_facade_url); + : new URL("./", request.credit_facade_url); if (revenueAPI) { const resp = await testRevenueAPI( @@ -71,7 +71,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { ); if (resp.type === "fail") { switch (resp.case) { - case "no-config": { + case TestRevenueErrorType.NO_CONFIG: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -79,7 +79,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { }); return; } - case "client-bad-request": { + case TestRevenueErrorType.CLIENT_BAD_REQUEST: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -87,7 +87,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { }); return; } - case "unauthorized": { + case TestRevenueErrorType.UNAUTHORIZED: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -95,7 +95,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { }); return; } - case "not-found": { + case TestRevenueErrorType.NOT_FOUND: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -103,7 +103,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { }); return; } - case "error": { + case TestRevenueErrorType.GENERIC_ERROR: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -112,7 +112,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { return; } default: { - assertUnreachable(resp) + assertUnreachable(resp.case); } } } @@ -136,92 +136,102 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { ); } +export enum TestRevenueErrorType { + NO_CONFIG, + CLIENT_BAD_REQUEST, + UNAUTHORIZED, + NOT_FOUND, + GENERIC_ERROR, +} + export async function testRevenueAPI( revenueAPI: URL, creds: FacadeCredentials | undefined, -): Promise< - | OperationOk<void> - | OperationFail<"no-config"> - | OperationFail<"client-bad-request"> - | OperationFail<"unauthorized"> - | OperationFail<"not-found"> - | OperationFail<"error"> -> { +): Promise<OperationOk<void> | OperationFail<TestRevenueErrorType>> { const api = new TalerRevenueHttpClient( revenueAPI.href, new BrowserFetchHttpLib(), ); + const auth = + creds === undefined + ? undefined + : creds.type === "none" + ? undefined + : creds.type === "basic" + ? { + username: creds.username, + password: creds.password, + } + : undefined; + try { - const config = await api.getConfig(); + const config = await api.getConfig(auth); + if (config.type === "fail") { - return { - type: "fail", - case: "no-config", - detail: { - code: 1, - }, - }; + switch (config.case) { + case HttpStatusCode.Unauthorized: { + return { + type: "fail", + case: TestRevenueErrorType.UNAUTHORIZED, + detail: { + code: 1, + }, + }; + } + case HttpStatusCode.NotFound: { + return { + type: "fail", + case: TestRevenueErrorType.NO_CONFIG, + detail: { + code: 1, + }, + }; + } + } + } + + const history = await api.getHistory(auth); + + if (history.type === "fail") { + switch (history.case) { + case HttpStatusCode.BadRequest: { + return { + type: "fail", + case: TestRevenueErrorType.CLIENT_BAD_REQUEST, + detail: { + code: 1, + }, + }; + } + case HttpStatusCode.Unauthorized: { + return { + type: "fail", + case: TestRevenueErrorType.UNAUTHORIZED, + detail: { + code: 1, + }, + }; + } + case HttpStatusCode.NotFound: { + return { + type: "fail", + case: TestRevenueErrorType.NOT_FOUND, + detail: { + code: 1, + }, + }; + } + } } } catch (err) { if (err instanceof TalerError) { return { type: "fail", - case: "error", + case: TestRevenueErrorType.GENERIC_ERROR, detail: err.errorDetail, }; } } - if (creds) { - const auth = - creds.type === "basic" - ? { - username: creds.username, - password: creds.password, - } - : undefined; - - try { - const history = await api.getHistory(auth); - if (history.type === "fail") { - switch (history.case) { - case HttpStatusCode.BadRequest: { - return { - type: "fail", - case: "client-bad-request", - detail: { - code: 1, - }, - }; - } - case HttpStatusCode.Unauthorized: { - return { - type: "fail", - case: "unauthorized", - detail: { - code: 1, - }, - }; - } - case HttpStatusCode.NotFound: { - return { - type: "fail", - case: "not-found", - detail: { - code: 1, - }, - }; - } - } - } - } catch (err) { - if (err instanceof TalerError) { - return { - type: "fail", - case: "error", - detail: err.errorDetail, - }; - } - } - } + return opFixedSuccess(undefined); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx index 6dd264f29..1a8e9bdc1 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -33,8 +33,7 @@ import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -type Entity = TalerMerchantApi.BankAccountEntry - & WithId; +type Entity = TalerMerchantApi.BankAccountEntry & WithId; const accountAuthType = ["unedit", "none", "basic"]; interface Props { @@ -43,32 +42,56 @@ interface Props { account: Entity; } - export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const [state, setState] = useState<Partial<TalerMerchantApi.AccountPatchDetails>>(account); + const [state, setState] = + useState<Partial<TalerMerchantApi.AccountPatchDetails>>(account); // @ts-expect-error "unedit" is fine since is part of the accountAuthType values if (state.credit_facade_credentials?.type === "unedit") { // we use this to set creds to undefined but server don't get this type - state.credit_facade_credentials = undefined + state.credit_facade_credentials = undefined; } + const facadeURL = safeConvertURL(state.credit_facade_url); + const errors: FormErrors<TalerMerchantApi.AccountPatchDetails> = { - credit_facade_url: !state.credit_facade_url ? undefined : !isValidURL(state.credit_facade_url) ? i18n.str`invalid url` : undefined, + credit_facade_url: !state.credit_facade_url + ? undefined + : !facadeURL + ? i18n.str`Invalid url` + : !facadeURL.href.endsWith("/") + ? i18n.str`URL should end with a '/'` + : facadeURL.searchParams.size > 0 + ? i18n.str`URL should not contain params` + : facadeURL.hash + ? i18n.str`URL should not hash param` + : undefined, credit_facade_credentials: undefinedIfEmpty({ + username: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !state.credit_facade_credentials.username + ? i18n.str`required` + : undefined, - username: state.credit_facade_credentials?.type !== "basic" ? undefined - : !state.credit_facade_credentials.username ? i18n.str`required` : undefined, - - password: state.credit_facade_credentials?.type !== "basic" ? undefined - : !state.credit_facade_credentials.password ? i18n.str`required` : undefined, - - repeatPassword: state.credit_facade_credentials?.type !== "basic" ? undefined - : !(state.credit_facade_credentials as any).repeatPassword ? i18n.str`required` : - (state.credit_facade_credentials as any).repeatPassword !== state.credit_facade_credentials.password ? i18n.str`doesn't match` + password: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !state.credit_facade_credentials.password + ? i18n.str`required` : undefined, + + repeatPassword: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !(state.credit_facade_credentials as any).repeatPassword + ? i18n.str`required` + : (state.credit_facade_credentials as any).repeatPassword !== + state.credit_facade_credentials.password + ? i18n.str`doesn't match` + : undefined, }), }; @@ -78,18 +101,25 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { const submitForm = () => { if (hasErrors) return Promise.reject(); - - const credit_facade_url = !state.credit_facade_url ? undefined : new URL("/", state.credit_facade_url).href - - const credit_facade_credentials: TalerMerchantApi.FacadeCredentials | undefined = - credit_facade_url == undefined || state.credit_facade_credentials === undefined ? undefined : - state.credit_facade_credentials.type === "basic" ? { - type: "basic", - password: state.credit_facade_credentials.password, - username: state.credit_facade_credentials.username, - } : { - type: "none" - }; + const credit_facade_url = !state.credit_facade_url + ? undefined + : facadeURL?.href; + + const credit_facade_credentials: + | TalerMerchantApi.FacadeCredentials + | undefined = + credit_facade_url == undefined || + state.credit_facade_credentials === undefined + ? undefined + : state.credit_facade_credentials.type === "basic" + ? { + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } + : { + type: "none", + }; return onUpdate({ credit_facade_credentials, credit_facade_url }); }; @@ -140,7 +170,7 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { toStr={(str) => { if (str === "none") return "Without authentication"; if (str === "basic") return "With authentication"; - return "Do not change" + return "Do not change"; }} /> {state.credit_facade_credentials?.type === "basic" ? ( @@ -191,11 +221,12 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { ); } -function isValidURL(s: string): boolean { +//TODO: move to utils +export function safeConvertURL(s?: string): URL | undefined { + if (!s) return undefined; try { - const u = new URL("/", s) - return true; + return new URL(s); } catch (e) { - return false; + return undefined; } } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx index cf1d5b0c7..519c9f56a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -35,7 +35,7 @@ import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; -import { testRevenueAPI } from "../create/index.js"; +import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js"; export type Entity = TalerMerchantApi.AccountPatchDetails & WithId; @@ -83,7 +83,7 @@ export default function UpdateValidator({ onUpdate={async (request) => { const revenueAPI = !request.credit_facade_url ? undefined - : new URL("/", request.credit_facade_url); + : new URL("./", request.credit_facade_url); if (revenueAPI) { const resp = await testRevenueAPI( @@ -92,7 +92,7 @@ export default function UpdateValidator({ ); if (resp.type === "fail") { switch (resp.case) { - case "no-config": { + case TestRevenueErrorType.NO_CONFIG: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -100,7 +100,7 @@ export default function UpdateValidator({ }); return; } - case "client-bad-request": { + case TestRevenueErrorType.CLIENT_BAD_REQUEST: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -108,7 +108,7 @@ export default function UpdateValidator({ }); return; } - case "unauthorized": { + case TestRevenueErrorType.UNAUTHORIZED: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -116,7 +116,7 @@ export default function UpdateValidator({ }); return; } - case "not-found": { + case TestRevenueErrorType.NOT_FOUND: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -124,7 +124,7 @@ export default function UpdateValidator({ }); return; } - case "error": { + case TestRevenueErrorType.GENERIC_ERROR: { setNotif({ message: i18n.str`Could not create account`, type: "ERROR", @@ -133,7 +133,7 @@ export default function UpdateValidator({ return; } default: { - assertUnreachable(resp) + assertUnreachable(resp.case) } } } diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts index b195e8c8f..34afe7d86 100644 --- a/packages/taler-util/src/http-client/bank-revenue.ts +++ b/packages/taler-util/src/http-client/bank-revenue.ts @@ -44,6 +44,10 @@ export type TalerBankRevenueErrorsByMethod< prop extends keyof TalerRevenueHttpClient, > = FailCasesByMethod<TalerRevenueHttpClient, prop>; +type UsernameAndPassword = { + username: string; + password: string; +}; /** * The API is used by the merchant (or other parties) to query * for incoming transactions to their account. @@ -69,33 +73,34 @@ export class TalerRevenueHttpClient { * https://docs.taler.net/core/api-bank-revenue.html#get--config * */ - async getConfig() { + async getConfig(auth?: UsernameAndPassword) { const url = new URL(`config`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", + headers: { + Authorization: auth + ? makeBasicAuthHeader(auth.username, auth.password) + : undefined, + }, }); switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForRevenueConfig()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: return opUnknownFailure(resp, await readTalerErrorResponse(resp)); } } - /** * https://docs.taler.net/core/api-bank-revenue.html#get--history * * @returns */ async getHistory( - auth: - | { - username: string; - password: string; - } - | undefined, + auth?: UsernameAndPassword, params?: PaginationParams & LongPollParams, ) { const url = new URL(`history`, this.baseUrl); |