From a8c5a9696c1735a178158cbc9ac4f9bb4b6f013d Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Wed, 8 Feb 2023 17:41:19 -0300
Subject: impl accout management and refactor
---
packages/demobank-ui/src/pages/AdminPage.tsx | 707 +++++++++++++++++++++++++++
1 file changed, 707 insertions(+)
create mode 100644 packages/demobank-ui/src/pages/AdminPage.tsx
(limited to 'packages/demobank-ui/src/pages/AdminPage.tsx')
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx
new file mode 100644
index 000000000..9efd37f12
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AdminPage.tsx
@@ -0,0 +1,707 @@
+/*
+ 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
+ */
+
+import { parsePaytoUri, TranslatedString } from "@gnu-taler/taler-util";
+import {
+ HttpResponsePaginated,
+ RequestError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorMessage, usePageContext } from "../context/pageState.js";
+import {
+ useAccountDetails,
+ useAccounts,
+ useAdminAccountAPI,
+} from "../hooks/circuit.js";
+import {
+ PartialButDefined,
+ undefinedIfEmpty,
+ WithIntermediate,
+} from "../utils.js";
+import { ErrorBanner } from "./BankFrame.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+
+const charset =
+ "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+const upperIdx = charset.indexOf("A");
+
+function randomPassword(): string {
+ const random = Array.from({ length: 16 }).map(() => {
+ return charset.charCodeAt(Math.random() * charset.length);
+ });
+ // first char can't be upper
+ const charIdx = charset.indexOf(String.fromCharCode(random[0]));
+ random[0] =
+ charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0];
+ return String.fromCharCode(...random);
+}
+
+interface Props {
+ onLoadNotOk: (error: HttpResponsePaginated) => VNode;
+}
+/**
+ * Query account information and show QR code if there is pending withdrawal
+ */
+export function AdminPage({ onLoadNotOk }: Props): VNode {
+ const [account, setAccount] = useState();
+ const [showDetails, setShowDetails] = useState();
+ const [updatePassword, setUpdatePassword] = useState();
+ const [createAccount, setCreateAccount] = useState(false);
+ const { pageStateSetter } = usePageContext();
+
+ function showInfoMessage(info: TranslatedString): void {
+ pageStateSetter((prev) => ({
+ ...prev,
+ info,
+ }));
+ }
+
+ const result = useAccounts({ account });
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return ;
+ if (!result.ok) {
+ return onLoadNotOk(result);
+ }
+
+ const { customers } = result.data;
+
+ if (showDetails) {
+ return (
+ {
+ showInfoMessage(i18n.str`Account updated`);
+ setShowDetails(undefined);
+ }}
+ onClear={() => {
+ setShowDetails(undefined);
+ }}
+ />
+ );
+ }
+ if (updatePassword) {
+ return (
+ {
+ showInfoMessage(i18n.str`Password changed`);
+ setUpdatePassword(undefined);
+ }}
+ onClear={() => {
+ setUpdatePassword(undefined);
+ }}
+ />
+ );
+ }
+ if (createAccount) {
+ return (
+ setCreateAccount(false)}
+ onCreateSuccess={(password) => {
+ showInfoMessage(
+ i18n.str`Account created with password "${password}"`,
+ );
+ setCreateAccount(false);
+ }}
+ />
+ );
+ }
+ return (
+
+
+
+ Admin panel
+
+
+
+
+
+
+
+ {
+ e.preventDefault();
+
+ setCreateAccount(true);
+ }}
+ />
+
+
+
+
+
+
+ {i18n.str`Accounts:`}
+
+
+
+
+ );
+}
+
+const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
+const EMAIL_REGEX =
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
+
+function initializeFromTemplate(
+ account: SandboxBackend.Circuit.CircuitAccountData | undefined,
+): WithIntermediate {
+ const emptyAccount = {
+ cashout_address: undefined,
+ iban: undefined,
+ name: undefined,
+ username: undefined,
+ contact_data: undefined,
+ };
+ const emptyContact = {
+ email: undefined,
+ phone: undefined,
+ };
+
+ const initial: PartialButDefined =
+ structuredClone(account) ?? emptyAccount;
+ if (typeof initial.contact_data === "undefined") {
+ initial.contact_data = emptyContact;
+ }
+ initial.contact_data.email;
+ return initial as any;
+}
+
+function UpdateAccountPassword({
+ account,
+ onClear,
+ onUpdateSuccess,
+ onLoadNotOk,
+}: {
+ onLoadNotOk: (error: HttpResponsePaginated) => VNode;
+ onClear: () => void;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useAccountDetails(account);
+ const { changePassword } = useAdminAccountAPI();
+ const [password, setPassword] = useState();
+ const [repeat, setRepeat] = useState();
+ const [error, saveError] = useState();
+
+ if (result.clientError) {
+ if (result.isNotfound) return account not found
;
+ }
+ if (!result.ok) {
+ return onLoadNotOk(result);
+ }
+
+ const errors = undefinedIfEmpty({
+ password: !password ? i18n.str`required` : undefined,
+ repeat: !repeat
+ ? i18n.str`required`
+ : password !== repeat
+ ? i18n.str`password doesn't match`
+ : undefined,
+ });
+
+ return (
+
+
+
+ Admin panel
+
+
+ {error && (
+
saveError(undefined)} />
+ )}
+
+
+
+
+
+
+ );
+}
+
+function CreateNewAccount({
+ onClose,
+ onCreateSuccess,
+}: {
+ onClose: () => void;
+ onCreateSuccess: (password: string) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { createAccount } = useAdminAccountAPI();
+ const [submitAccount, setSubmitAccount] = useState<
+ SandboxBackend.Circuit.CircuitAccountData | undefined
+ >();
+ const [error, saveError] = useState();
+ return (
+
+
+
+ Admin panel
+
+
+ {error && (
+
saveError(undefined)} />
+ )}
+
+ setSubmitAccount(a)}
+ />
+
+
+
+
+
+ );
+}
+
+function ShowAccountDetails({
+ account,
+ onClear,
+ onUpdateSuccess,
+ onLoadNotOk,
+}: {
+ onLoadNotOk: (error: HttpResponsePaginated) => VNode;
+ onClear: () => void;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useAccountDetails(account);
+ const { updateAccount } = useAdminAccountAPI();
+ const [update, setUpdate] = useState(false);
+ const [submitAccount, setSubmitAccount] = useState<
+ SandboxBackend.Circuit.CircuitAccountData | undefined
+ >();
+ const [error, saveError] = useState();
+
+ if (result.clientError) {
+ if (result.isNotfound) return account not found
;
+ }
+ if (!result.ok) {
+ return onLoadNotOk(result);
+ }
+
+ return (
+
+
+
+ Admin panel
+
+
+ {error && (
+
saveError(undefined)} />
+ )}
+ setSubmitAccount(a)}
+ />
+
+
+
+
+
+ );
+}
+
+/**
+ * Create valid account object to update or create
+ * Take template as initial values for the form
+ * Purpose indicate if all field al read only (show), part of them (update)
+ * or none (create)
+ * @param param0
+ * @returns
+ */
+function AccountForm({
+ template,
+ purpose,
+ onChange,
+}: {
+ template: SandboxBackend.Circuit.CircuitAccountData | undefined;
+ onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
+ purpose: "create" | "update" | "show";
+}): VNode {
+ const initial = initializeFromTemplate(template);
+ const [form, setForm] = useState(initial);
+ const [errors, setErrors] = useState(undefined);
+ const { i18n } = useTranslationContext();
+
+ function updateForm(newForm: typeof initial): void {
+ const parsed = !newForm.cashout_address
+ ? undefined
+ : parsePaytoUri(newForm.cashout_address);
+
+ const validationResult = undefinedIfEmpty({
+ cashout_address: !newForm.cashout_address
+ ? i18n.str`required`
+ : !parsed
+ ? i18n.str`does not follow the pattern`
+ : !parsed.isKnown || parsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !IBAN_REGEX.test(parsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : undefined,
+ contact_data: {
+ email: !newForm.contact_data.email
+ ? undefined
+ : !EMAIL_REGEX.test(newForm.contact_data.email)
+ ? i18n.str`it should be an email`
+ : undefined,
+ phone: !newForm.contact_data.phone
+ ? undefined
+ : !newForm.contact_data.phone.startsWith("+")
+ ? i18n.str`should start with +`
+ : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
+ ? i18n.str`phone number can't have other than numbers`
+ : undefined,
+ },
+ iban: !newForm.iban
+ ? i18n.str`required`
+ : !IBAN_REGEX.test(newForm.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : undefined,
+ name: !newForm.name ? i18n.str`required` : undefined,
+ username: !newForm.username ? i18n.str`required` : undefined,
+ });
+
+ setErrors(validationResult);
+ setForm(newForm);
+ onChange(validationResult === undefined ? undefined : (newForm as any));
+ }
+
+ return (
+
+ );
+}
+
+function handleError(
+ error: unknown,
+ saveError: (e: ErrorMessage) => void,
+ i18n: ReturnType["i18n"],
+): void {
+ if (error instanceof RequestError) {
+ const payload = error.info.error as SandboxBackend.SandboxError;
+ saveError({
+ title: error.info.serverError
+ ? i18n.str`Server had an error`
+ : i18n.str`Server didn't accept the request`,
+ description: payload.error.description,
+ });
+ } else if (error instanceof Error) {
+ saveError({
+ title: i18n.str`Could not update account`,
+ description: error.message,
+ });
+ } else {
+ saveError({
+ title: i18n.str`Error, please report`,
+ debug: JSON.stringify(error),
+ });
+ }
+}
--
cgit v1.2.3