aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcopy-demobank-into-prebuilt.sh2
-rw-r--r--packages/demobank-ui/README.md2
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts16
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts10
-rw-r--r--packages/demobank-ui/src/pages/AdminPage.tsx214
-rw-r--r--packages/demobank-ui/src/utils.ts205
6 files changed, 339 insertions, 110 deletions
diff --git a/copy-demobank-into-prebuilt.sh b/copy-demobank-into-prebuilt.sh
index 5927eac96..3fd4ec5c5 100755
--- a/copy-demobank-into-prebuilt.sh
+++ b/copy-demobank-into-prebuilt.sh
@@ -2,7 +2,7 @@
[ ! -d prebuilt ] && echo 'directory "prebuilt" not found. first checkout the prebuilt branch into a prebuilt directory' && exit 1
-for file in index.html index.js index.css; do
+for file in index.html index.js index.css logo-white-U55BSKA2.svg; do
cp packages/demobank-ui/dist/$file prebuilt/demobank/
done
diff --git a/packages/demobank-ui/README.md b/packages/demobank-ui/README.md
index b8f96c5ea..1732b5f38 100644
--- a/packages/demobank-ui/README.md
+++ b/packages/demobank-ui/README.md
@@ -18,6 +18,7 @@ By default, the demobank-ui points to `https://bank.demo.taler.net/demobanks/def
as the bank access API base URL.
This can be changed for testing by setting the URL via local storage (via your browser's devtools):
+
```
localStorage.setItem("bank-base-url", OTHER_URL);
```
@@ -35,6 +36,7 @@ to the default settings:
```
globalThis.talerDemobankSettings = {
+ backendBaseURL: "https://bank.demo.taler.net/demobanks/default/",
allowRegistrations: true,
bankName: "Taler Bank",
// Show explainer text and navbar to other demo sites
diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts
index 3f2981edf..3eaf1f186 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -42,26 +42,23 @@ export interface BackendCredentials {
}
interface LoggedIn extends BackendCredentials {
- url: string;
status: "loggedIn";
isUserAdministrator: boolean;
}
interface LoggedOut {
- url: string;
status: "loggedOut";
}
-const maybeRootPath = bankUiSettings.backendBaseURL;
-
export function getInitialBackendBaseURL(): string {
const overrideUrl = localStorage.getItem("bank-base-url");
- return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath);
+ return canonicalizeBaseUrl(
+ overrideUrl ? overrideUrl : bankUiSettings.backendBaseURL,
+ );
}
export const defaultState: BackendState = {
status: "loggedOut",
- url: getInitialBackendBaseURL(),
};
export interface BackendStateHandler {
@@ -91,13 +88,12 @@ export function useBackendState(): BackendStateHandler {
return {
state,
logOut() {
- update(JSON.stringify({ ...defaultState, url: state.url }));
+ update(JSON.stringify({ ...defaultState }));
},
logIn(info) {
//admin is defined by the username
const nextState: BackendState = {
status: "loggedIn",
- url: state.url,
...info,
isUserAdministrator: info.username === "admin",
};
@@ -125,7 +121,7 @@ export function usePublicBackend(): useBackendType {
const { state } = useBackendContext();
const { request: requestHandler } = useApiContext();
- const baseUrl = state.url;
+ const baseUrl = getInitialBackendBaseURL();
const request = useCallback(
function requestImpl<T>(
@@ -201,7 +197,7 @@ export function useAuthenticatedBackend(): useBackendType {
const { request: requestHandler } = useApiContext();
const creds = state.status === "loggedIn" ? state : undefined;
- const baseUrl = state.url;
+ const baseUrl = getInitialBackendBaseURL();
const request = useCallback(
function requestImpl<T>(
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts
index c2563adb4..423ed1a5b 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -24,7 +24,11 @@ import {
import { useEffect, useMemo, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
-import { useAuthenticatedBackend, useMatchMutate } from "./backend.js";
+import {
+ getInitialBackendBaseURL,
+ useAuthenticatedBackend,
+ useMatchMutate,
+} from "./backend.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import _useSWR, { SWRHook } from "swr";
@@ -210,10 +214,10 @@ export interface CircuitAccountAPI {
async function getBusinessStatus(
request: ReturnType<typeof useApiContext>["request"],
- url: string,
basicAuth: { username: string; password: string },
): Promise<boolean> {
try {
+ const url = getInitialBackendBaseURL();
const result = await request<
HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>
>(url, `circuit-api/accounts/${basicAuth.username}`, { basicAuth });
@@ -234,7 +238,7 @@ export function useBusinessAccountFlag(): boolean | undefined {
useEffect(() => {
if (!creds) return;
- getBusinessStatus(request, state.url, creds)
+ getBusinessStatus(request, creds)
.then((result) => {
setIsBusiness(result);
})
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx
index 2a5701a95..3d0c09cbf 100644
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ b/packages/demobank-ui/src/pages/AdminPage.tsx
@@ -40,6 +40,7 @@ import {
PartialButDefined,
RecursivePartial,
undefinedIfEmpty,
+ validateIBAN,
WithIntermediate,
} from "../utils.js";
import { ErrorBannerFloat } from "./BankFrame.js";
@@ -230,74 +231,78 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
</p>
<section id="main">
- <article>
- <h2>{i18n.str`Accounts:`}</h2>
- <div class="results">
- <table class="pure-table pure-table-striped">
- <thead>
- <tr>
- <th>{i18n.str`Username`}</th>
- <th>{i18n.str`Name`}</th>
- <th></th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- {customers.map((item, idx) => {
- return (
- <tr key={idx}>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setShowDetails(item.username);
- }}
- >
- {item.username}
- </a>
- </td>
- <td>{item.name}</td>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setUpdatePassword(item.username);
- }}
- >
- change password
- </a>
- </td>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setShowCashouts(item.username);
- }}
- >
- cashouts
- </a>
- </td>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setRemoveAccount(item.username);
- }}
- >
- remove
- </a>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </article>
+ {!customers.length ? (
+ <div></div>
+ ) : (
+ <article>
+ <h2>{i18n.str`Accounts:`}</h2>
+ <div class="results">
+ <table class="pure-table pure-table-striped">
+ <thead>
+ <tr>
+ <th>{i18n.str`Username`}</th>
+ <th>{i18n.str`Name`}</th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {customers.map((item, idx) => {
+ return (
+ <tr key={idx}>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowDetails(item.username);
+ }}
+ >
+ {item.username}
+ </a>
+ </td>
+ <td>{item.name}</td>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setUpdatePassword(item.username);
+ }}
+ >
+ change password
+ </a>
+ </td>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowCashouts(item.username);
+ }}
+ >
+ cashouts
+ </a>
+ </td>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setRemoveAccount(item.username);
+ }}
+ >
+ remove
+ </a>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ </article>
+ )}
</section>
</Fragment>
);
@@ -835,15 +840,15 @@ function AccountForm({
? i18n.str`only "IBAN" target are supported`
: !IBAN_REGEX.test(parsed.iban)
? i18n.str`IBAN should have just uppercased letters and numbers`
- : undefined,
+ : validateIBAN(parsed.iban, i18n),
contact_data: undefinedIfEmpty({
email: !newForm.contact_data?.email
- ? undefined
+ ? i18n.str`required`
: !EMAIL_REGEX.test(newForm.contact_data.email)
? i18n.str`it should be an email`
: undefined,
phone: !newForm.contact_data?.phone
- ? undefined
+ ? i18n.str`required`
: !newForm.contact_data.phone.startsWith("+")
? i18n.str`should start with +`
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
@@ -851,10 +856,10 @@ function AccountForm({
: undefined,
}),
iban: !newForm.iban
- ? i18n.str`required`
+ ? undefined //optional field
: !IBAN_REGEX.test(newForm.iban)
? i18n.str`IBAN should have just uppercased letters and numbers`
- : undefined,
+ : validateIBAN(newForm.iban, i18n),
name: !newForm.name ? i18n.str`required` : undefined,
username: !newForm.username ? i18n.str`required` : undefined,
});
@@ -866,7 +871,10 @@ function AccountForm({
return (
<form class="pure-form">
<fieldset>
- <label for="username">{i18n.str`Username`}</label>
+ <label for="username">
+ {i18n.str`Username`}
+ {purpose === "create" && <b style={{ color: "red" }}>*</b>}
+ </label>
<input
name="username"
type="text"
@@ -876,14 +884,17 @@ function AccountForm({
form.username = e.currentTarget.value;
updateForm(structuredClone(form));
}}
- />
+ />{" "}
<ShowInputErrorLabel
message={errors?.username}
isDirty={form.username !== undefined}
/>
</fieldset>
<fieldset>
- <label>{i18n.str`Name`}</label>
+ <label>
+ {i18n.str`Name`}
+ {purpose === "create" && <b style={{ color: "red" }}>*</b>}
+ </label>
<input
disabled={purpose !== "create"}
value={form.name ?? ""}
@@ -897,23 +908,28 @@ function AccountForm({
isDirty={form.name !== undefined}
/>
</fieldset>
+ {purpose !== "create" && (
+ <fieldset>
+ <label>{i18n.str`Internal IBAN`}</label>
+ <input
+ disabled={true}
+ value={form.iban ?? ""}
+ onChange={(e) => {
+ form.iban = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.iban}
+ isDirty={form.iban !== undefined}
+ />
+ </fieldset>
+ )}
<fieldset>
- <label>{i18n.str`Internal IBAN`}</label>
- <input
- disabled={purpose !== "create"}
- value={form.iban ?? ""}
- onChange={(e) => {
- form.iban = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.iban}
- isDirty={form.iban !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>{i18n.str`Email`}</label>
+ <label>
+ {i18n.str`Email`}
+ {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
+ </label>
<input
disabled={purpose === "show"}
value={form.contact_data.email ?? ""}
@@ -928,7 +944,10 @@ function AccountForm({
/>
</fieldset>
<fieldset>
- <label>{i18n.str`Phone`}</label>
+ <label>
+ {i18n.str`Phone`}
+ {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
+ </label>
<input
disabled={purpose === "show"}
value={form.contact_data.phone ?? ""}
@@ -943,12 +962,15 @@ function AccountForm({
/>
</fieldset>
<fieldset>
- <label>{i18n.str`Cashout address`}</label>
+ <label>
+ {i18n.str`Cashout address`}
+ {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
+ </label>
<input
disabled={purpose === "show"}
- value={form.cashout_address ?? ""}
+ value={(form.cashout_address ?? "").substring("payto://iban/".length)}
onChange={(e) => {
- form.cashout_address = e.currentTarget.value;
+ form.cashout_address = "payto://iban/" + e.currentTarget.value;
updateForm(structuredClone(form));
}}
/>
diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts
index 81dd450a4..0796db65d 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -161,3 +161,208 @@ export function buildRequestErrorMessage(
}
return result;
}
+
+export const COUNTRY_TABLE = {
+ AE: "U.A.E.",
+ AF: "Afghanistan",
+ AL: "Albania",
+ AM: "Armenia",
+ AN: "Netherlands Antilles",
+ AR: "Argentina",
+ AT: "Austria",
+ AU: "Australia",
+ AZ: "Azerbaijan",
+ BA: "Bosnia and Herzegovina",
+ BD: "Bangladesh",
+ BE: "Belgium",
+ BG: "Bulgaria",
+ BH: "Bahrain",
+ BN: "Brunei Darussalam",
+ BO: "Bolivia",
+ BR: "Brazil",
+ BT: "Bhutan",
+ BY: "Belarus",
+ BZ: "Belize",
+ CA: "Canada",
+ CG: "Congo",
+ CH: "Switzerland",
+ CI: "Cote d'Ivoire",
+ CL: "Chile",
+ CM: "Cameroon",
+ CN: "People's Republic of China",
+ CO: "Colombia",
+ CR: "Costa Rica",
+ CS: "Serbia and Montenegro",
+ CZ: "Czech Republic",
+ DE: "Germany",
+ DK: "Denmark",
+ DO: "Dominican Republic",
+ DZ: "Algeria",
+ EC: "Ecuador",
+ EE: "Estonia",
+ EG: "Egypt",
+ ER: "Eritrea",
+ ES: "Spain",
+ ET: "Ethiopia",
+ FI: "Finland",
+ FO: "Faroe Islands",
+ FR: "France",
+ GB: "United Kingdom",
+ GD: "Caribbean",
+ GE: "Georgia",
+ GL: "Greenland",
+ GR: "Greece",
+ GT: "Guatemala",
+ HK: "Hong Kong",
+ // HK: "Hong Kong S.A.R.",
+ HN: "Honduras",
+ HR: "Croatia",
+ HT: "Haiti",
+ HU: "Hungary",
+ ID: "Indonesia",
+ IE: "Ireland",
+ IL: "Israel",
+ IN: "India",
+ IQ: "Iraq",
+ IR: "Iran",
+ IS: "Iceland",
+ IT: "Italy",
+ JM: "Jamaica",
+ JO: "Jordan",
+ JP: "Japan",
+ KE: "Kenya",
+ KG: "Kyrgyzstan",
+ KH: "Cambodia",
+ KR: "South Korea",
+ KW: "Kuwait",
+ KZ: "Kazakhstan",
+ LA: "Laos",
+ LB: "Lebanon",
+ LI: "Liechtenstein",
+ LK: "Sri Lanka",
+ LT: "Lithuania",
+ LU: "Luxembourg",
+ LV: "Latvia",
+ LY: "Libya",
+ MA: "Morocco",
+ MC: "Principality of Monaco",
+ MD: "Moldava",
+ // MD: "Moldova",
+ ME: "Montenegro",
+ MK: "Former Yugoslav Republic of Macedonia",
+ ML: "Mali",
+ MM: "Myanmar",
+ MN: "Mongolia",
+ MO: "Macau S.A.R.",
+ MT: "Malta",
+ MV: "Maldives",
+ MX: "Mexico",
+ MY: "Malaysia",
+ NG: "Nigeria",
+ NI: "Nicaragua",
+ NL: "Netherlands",
+ NO: "Norway",
+ NP: "Nepal",
+ NZ: "New Zealand",
+ OM: "Oman",
+ PA: "Panama",
+ PE: "Peru",
+ PH: "Philippines",
+ PK: "Islamic Republic of Pakistan",
+ PL: "Poland",
+ PR: "Puerto Rico",
+ PT: "Portugal",
+ PY: "Paraguay",
+ QA: "Qatar",
+ RE: "Reunion",
+ RO: "Romania",
+ RS: "Serbia",
+ RU: "Russia",
+ RW: "Rwanda",
+ SA: "Saudi Arabia",
+ SE: "Sweden",
+ SG: "Singapore",
+ SI: "Slovenia",
+ SK: "Slovak",
+ SN: "Senegal",
+ SO: "Somalia",
+ SR: "Suriname",
+ SV: "El Salvador",
+ SY: "Syria",
+ TH: "Thailand",
+ TJ: "Tajikistan",
+ TM: "Turkmenistan",
+ TN: "Tunisia",
+ TR: "Turkey",
+ TT: "Trinidad and Tobago",
+ TW: "Taiwan",
+ TZ: "Tanzania",
+ UA: "Ukraine",
+ US: "United States",
+ UY: "Uruguay",
+ VA: "Vatican",
+ VE: "Venezuela",
+ VN: "Viet Nam",
+ YE: "Yemen",
+ ZA: "South Africa",
+ ZW: "Zimbabwe",
+};
+
+/**
+ * An IBAN is validated by converting it into an integer and performing a
+ * basic mod-97 operation (as described in ISO 7064) on it.
+ * If the IBAN is valid, the remainder equals 1.
+ *
+ * The algorithm of IBAN validation is as follows:
+ * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid
+ * 2.- Move the four initial characters to the end of the string
+ * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35
+ * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97
+ *
+ * If the remainder is 1, the check digit test is passed and the IBAN might be valid.
+ *
+ */
+export function validateIBAN(
+ iban: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ // Check total length
+ if (iban.length < 4)
+ return i18n.str`IBAN numbers usually have more that 4 digits`;
+ if (iban.length > 34)
+ return i18n.str`IBAN numbers usually have less that 34 digits`;
+
+ const A_code = "A".charCodeAt(0);
+ const Z_code = "Z".charCodeAt(0);
+ const IBAN = iban.toUpperCase();
+ // check supported country
+ const code = IBAN.substring(0, 2);
+ const found = code in COUNTRY_TABLE;
+ if (!found) return i18n.str`IBAN country code not found`;
+
+ // 2.- Move the four initial characters to the end of the string
+ const step2 = IBAN.substring(4) + iban.substring(0, 4);
+ const step3 = Array.from(step2)
+ .map((letter) => {
+ const code = letter.charCodeAt(0);
+ if (code < A_code || code > Z_code) return letter;
+ return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
+ })
+ .join("");
+
+ const checksum = calculate_iban_checksum(step3);
+ if (checksum !== 1)
+ return i18n.str`IBAN number is not valid, checksum is wrong`;
+ return undefined;
+}
+
+function calculate_iban_checksum(str: string): number {
+ const numberStr = str.substring(0, 5);
+ const rest = str.substring(5);
+ const number = parseInt(numberStr, 10);
+ const result = number % 97;
+ if (rest.length > 0) {
+ return calculate_iban_checksum(`${result}${rest}`);
+ }
+ return result;
+}