no-fix: moving out registration page

This commit is contained in:
Sebastian 2022-12-07 11:36:20 -03:00
parent 5d5b63416b
commit e4a2937f2a
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
5 changed files with 309 additions and 247 deletions

View File

@ -1,9 +1,26 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { createHashHistory } from "history";
import { h, VNode } from "preact";
import Router, { route, Route } from "preact-router";
import { useEffect } from "preact/hooks";
import { AccountPage, RegistrationPage } from "./home/index.js";
import { AccountPage } from "./home/index.js";
import { PublicHistoriesPage } from "./home/PublicHistoriesPage.js";
import { RegistrationPage } from "./home/RegistrationPage.js";
export function Routing(): VNode {
const history = createHashHistory();

View File

@ -0,0 +1,254 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Fragment, h, VNode } from "preact";
import { route } from "preact-router";
import { StateUpdater, useState } from "preact/hooks";
import { PageStateType, usePageContext } from "../../context/pageState.js";
import { useTranslationContext } from "../../context/translation.js";
import { BackendStateType, useBackendState } from "../../hooks/backend.js";
import { bankUiSettings } from "../../settings.js";
import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js";
import { BankFrame } from "./BankFrame.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
export function RegistrationPage(): VNode {
const { i18n } = useTranslationContext();
if (!bankUiSettings.allowRegistrations) {
return (
<BankFrame>
<p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
</BankFrame>
);
}
return (
<BankFrame>
<RegistrationForm />
</BankFrame>
);
}
/**
* Collect and submit registration data.
*/
function RegistrationForm(): VNode {
const [backendState, backendStateSetter] = useBackendState();
const { pageState, pageStateSetter } = usePageContext();
const [username, setUsername] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
const errors = undefinedIfEmpty({
username: !username ? i18n.str`Missing username` : undefined,
password: !password ? i18n.str`Missing password` : undefined,
repeatPassword: !repeatPassword
? i18n.str`Missing password`
: repeatPassword !== password
? i18n.str`Password don't match`
: undefined,
});
return (
<Fragment>
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
<article>
<div class="register-div">
<form action="javascript:void(0);" class="register-form" noValidate>
<div class="pure-form">
<h2>{i18n.str`Please register!`}</h2>
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-un">{i18n.str`Username:`}</label>
</p>
<input
id="register-un"
name="register-un"
type="text"
placeholder="Username"
value={username ?? ""}
onInput={(e): void => {
setUsername(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.username}
isDirty={username !== undefined}
/>
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-pw">{i18n.str`Password:`}</label>
</p>
<input
type="password"
name="register-pw"
id="register-pw"
placeholder="Password"
value={password ?? ""}
required
onInput={(e): void => {
setPassword(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.password}
isDirty={password !== undefined}
/>
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-repeat">{i18n.str`Repeat Password:`}</label>
</p>
<input
type="password"
style={{ marginBottom: 8 }}
name="register-repeat"
id="register-repeat"
placeholder="Same password"
value={repeatPassword ?? ""}
required
onInput={(e): void => {
setRepeatPassword(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.repeatPassword}
isDirty={repeatPassword !== undefined}
/>
<br />
<button
class="pure-button pure-button-primary btn-register"
disabled={!!errors}
onClick={() => {
if (!username || !password) return;
registrationCall(
{ username, password },
backendStateSetter, // will store BE URL, if OK.
pageStateSetter,
);
setUsername(undefined);
setPassword(undefined);
setRepeatPassword(undefined);
}}
>
{i18n.str`Register`}
</button>
{/* FIXME: should use a different color */}
<button
class="pure-button pure-button-secondary btn-cancel"
onClick={() => {
setUsername(undefined);
setPassword(undefined);
setRepeatPassword(undefined);
route("/account");
}}
>
{i18n.str`Cancel`}
</button>
</div>
</form>
</div>
</article>
</Fragment>
);
}
/**
* This function requests /register.
*
* This function is responsible to change two states:
* the backend's (to store the login credentials) and
* the page's (to indicate a successful login or a problem).
*/
async function registrationCall(
req: { username: string; password: string },
/**
* FIXME: figure out if the two following
* functions can be retrieved somewhat from
* the state.
*/
backendStateSetter: StateUpdater<BackendStateType | undefined>,
pageStateSetter: StateUpdater<PageStateType>,
): Promise<void> {
let baseUrl = getBankBackendBaseUrl();
/**
* If the base URL doesn't end with slash and the path
* is not empty, then the concatenation made by URL()
* drops the last path element.
*/
if (!baseUrl.endsWith("/")) baseUrl += "/";
const headers = new Headers();
headers.append("Content-Type", "application/json");
const url = new URL("access-api/testing/register", baseUrl);
let res: Response;
try {
res = await fetch(url.href, {
method: "POST",
body: JSON.stringify({
username: req.username,
password: req.password,
}),
headers,
});
} catch (error) {
console.log(
`Could not POST new registration to the bank (${url.href})`,
error,
);
pageStateSetter((prevState) => ({
...prevState,
error: {
title: `Registration failed, please report`,
debug: JSON.stringify(error),
},
}));
return;
}
if (!res.ok) {
const response = await res.json();
if (res.status === 409) {
pageStateSetter((prevState) => ({
...prevState,
error: {
title: `That username is already taken`,
debug: JSON.stringify(response),
},
}));
} else {
pageStateSetter((prevState) => ({
...prevState,
error: {
title: `New registration gave response error`,
debug: JSON.stringify(response),
},
}));
}
} else {
// registration was ok
pageStateSetter((prevState) => ({
...prevState,
isLoggedIn: true,
}));
backendStateSetter((prevState) => ({
...prevState,
url: baseUrl,
username: req.username,
password: req.password,
}));
route("/account");
}
}

View File

@ -0,0 +1,29 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Fragment, h, VNode } from "preact";
export function ShowInputErrorLabel({
isDirty,
message,
}: {
message: string | undefined;
isDirty: boolean;
}): VNode {
if (message && isDirty)
return <div style={{ marginTop: 8, color: "red" }}>{message}</div>;
return <Fragment />;
}

View File

@ -30,10 +30,12 @@ import { QrCodeSection } from "./QrCodeSection.js";
import {
getBankBackendBaseUrl,
getIbanFromPayto,
undefinedIfEmpty,
validateAmount,
} from "../../utils.js";
import { BankFrame } from "./BankFrame.js";
import { Transactions } from "./Transactions.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
/**
* FIXME:
@ -553,112 +555,10 @@ async function loginCall(
}));
}
/**
* This function requests /register.
*
* This function is responsible to change two states:
* the backend's (to store the login credentials) and
* the page's (to indicate a successful login or a problem).
*/
async function registrationCall(
req: { username: string; password: string },
/**
* FIXME: figure out if the two following
* functions can be retrieved somewhat from
* the state.
*/
backendStateSetter: StateUpdater<BackendStateType | undefined>,
pageStateSetter: StateUpdater<PageStateType>,
): Promise<void> {
let baseUrl = getBankBackendBaseUrl();
/**
* If the base URL doesn't end with slash and the path
* is not empty, then the concatenation made by URL()
* drops the last path element.
*/
if (!baseUrl.endsWith("/")) baseUrl += "/";
const headers = new Headers();
headers.append("Content-Type", "application/json");
const url = new URL("access-api/testing/register", baseUrl);
let res: Response;
try {
res = await fetch(url.href, {
method: "POST",
body: JSON.stringify({
username: req.username,
password: req.password,
}),
headers,
});
} catch (error) {
console.log(
`Could not POST new registration to the bank (${url.href})`,
error,
);
pageStateSetter((prevState) => ({
...prevState,
error: {
title: `Registration failed, please report`,
debug: JSON.stringify(error),
},
}));
return;
}
if (!res.ok) {
const response = await res.json();
if (res.status === 409) {
pageStateSetter((prevState) => ({
...prevState,
error: {
title: `That username is already taken`,
debug: JSON.stringify(response),
},
}));
} else {
pageStateSetter((prevState) => ({
...prevState,
error: {
title: `New registration gave response error`,
debug: JSON.stringify(response),
},
}));
}
} else {
// registration was ok
pageStateSetter((prevState) => ({
...prevState,
isLoggedIn: true,
}));
backendStateSetter((prevState) => ({
...prevState,
url: baseUrl,
username: req.username,
password: req.password,
}));
route("/account");
}
}
/**************************
* Functional components. *
*************************/
function ShowInputErrorLabel({
isDirty,
message,
}: {
message: string | undefined;
isDirty: boolean;
}): VNode {
if (message && isDirty)
return <div style={{ marginTop: 8, color: "red" }}>{message}</div>;
return <Fragment />;
}
function PaytoWireTransfer({
focus,
currency,
@ -1257,11 +1157,6 @@ function PaymentOptions({ currency }: { currency?: string }): VNode {
);
}
function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
? obj
: undefined;
}
/**
* Collect and submit login data.
*/
@ -1361,129 +1256,6 @@ function LoginForm(): VNode {
);
}
/**
* Collect and submit registration data.
*/
function RegistrationForm(): VNode {
const [backendState, backendStateSetter] = useBackendState();
const { pageState, pageStateSetter } = usePageContext();
const [username, setUsername] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
const errors = undefinedIfEmpty({
username: !username ? i18n.str`Missing username` : undefined,
password: !password ? i18n.str`Missing password` : undefined,
repeatPassword: !repeatPassword
? i18n.str`Missing password`
: repeatPassword !== password
? i18n.str`Password don't match`
: undefined,
});
return (
<Fragment>
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
<article>
<div class="register-div">
<form action="javascript:void(0);" class="register-form" noValidate>
<div class="pure-form">
<h2>{i18n.str`Please register!`}</h2>
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-un">{i18n.str`Username:`}</label>
</p>
<input
id="register-un"
name="register-un"
type="text"
placeholder="Username"
value={username ?? ""}
onInput={(e): void => {
setUsername(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.username}
isDirty={username !== undefined}
/>
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-pw">{i18n.str`Password:`}</label>
</p>
<input
type="password"
name="register-pw"
id="register-pw"
placeholder="Password"
value={password ?? ""}
required
onInput={(e): void => {
setPassword(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.password}
isDirty={username !== undefined}
/>
<p class="unameFieldLabel registerFieldLabel formFieldLabel">
<label for="register-repeat">{i18n.str`Repeat Password:`}</label>
</p>
<input
type="password"
style={{ marginBottom: 8 }}
name="register-repeat"
id="register-repeat"
placeholder="Same password"
value={repeatPassword ?? ""}
required
onInput={(e): void => {
setRepeatPassword(e.currentTarget.value);
}}
/>
<ShowInputErrorLabel
message={errors?.repeatPassword}
isDirty={username !== undefined}
/>
<br />
<button
class="pure-button pure-button-primary btn-register"
disabled={!!errors}
onClick={() => {
if (!username || !password) return;
registrationCall(
{ username, password },
backendStateSetter, // will store BE URL, if OK.
pageStateSetter,
);
setUsername(undefined);
setPassword(undefined);
setRepeatPassword(undefined);
}}
>
{i18n.str`Register`}
</button>
{/* FIXME: should use a different color */}
<button
class="pure-button pure-button-secondary btn-cancel"
onClick={() => {
setUsername(undefined);
setPassword(undefined);
setRepeatPassword(undefined);
route("/account");
}}
>
{i18n.str`Cancel`}
</button>
</div>
</form>
</div>
</article>
</Fragment>
);
}
/**
* Show only the account's balance. NOTE: the backend state
* is mostly needed to provide the user's credentials to POST
@ -1689,22 +1461,6 @@ function SWRWithCredentials(props: any): VNode {
);
}
export function RegistrationPage(): VNode {
const { i18n } = useTranslationContext();
if (!bankUiSettings.allowRegistrations) {
return (
<BankFrame>
<p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
</BankFrame>
);
}
return (
<BankFrame>
<RegistrationForm />
</BankFrame>
);
}
export function AccountPage(): VNode {
const [backendState, backendStateSetter] = useBackendState();
const { i18n } = useTranslationContext();

View File

@ -46,3 +46,9 @@ export function getBankBackendBaseUrl(): string {
console.log(`using bank base URL (${maybeRootPath})`);
return maybeRootPath;
}
export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
? obj
: undefined;
}