validate IBAN, removing internal iban from account form, add missing logo, do not save backend URL in login state
This commit is contained in:
parent
906eddd48a
commit
4de014927e
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>(
|
||||
|
@ -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);
|
||||
})
|
||||
|
@ -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));
|
||||
}}
|
||||
/>
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user