validate IBAN, removing internal iban from account form, add missing logo, do not save backend URL in login state

This commit is contained in:
Sebastian 2023-03-05 15:21:12 -03:00
parent 906eddd48a
commit 4de014927e
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
6 changed files with 339 additions and 110 deletions

View File

@ -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

View File

@ -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

View File

@ -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>(

View File

@ -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);
})

View File

@ -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));
}}
/>

View File

@ -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;
}