diff options
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages/CreateAccount.tsx')
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CreateAccount.tsx | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx new file mode 100644 index 000000000..f4904933b --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -0,0 +1,196 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + Button, + InputLine, + InternationalizationAPI, + LocalNotificationBanner, + UIHandlerId, + useLocalNotificationHandler, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { + FormErrors, + FormStatus, + FormValues, + RecursivePartial, + useFormState, +} from "../hooks/form.js"; +import { useOfficer } from "../hooks/officer.js"; +import { usePreferences } from "../hooks/preferences.js"; + +type FormType = { + password: string; + repeat: string; +}; +function createFormValidator( + i18n: InternationalizationAPI, + allowInsecurePassword: boolean, +) { + return function check( + state: RecursivePartial<FormValues<FormType>>, + ): FormStatus<FormType> { + const errors = undefinedIfEmpty<FormErrors<FormType>>({ + password: !state.password + ? i18n.str`required` + : allowInsecurePassword + ? undefined + : state.password.length < 8 + ? i18n.str`should have at least 8 characters` + : !state.password.match(/[a-z]/) && state.password.match(/[A-Z]/) + ? i18n.str`should have lowercase and uppercase characters` + : !state.password.match(/\d/) + ? i18n.str`should have numbers` + : !state.password.match(/[^a-zA-Z\d]/) + ? i18n.str`should have at least one character which is not a number or letter` + : undefined, + + repeat: !state.repeat + ? i18n.str`required` + : state.password !== state.repeat + ? i18n.str`doesn't match` + : undefined, + }); + + if (errors === undefined) { + const result: FormType = { + password: state.password!, + repeat: state.repeat!, + }; + return { + status: "ok", + result, + errors, + }; + } + const result: RecursivePartial<FormType> = { + password: state.password, + repeat: state.repeat, + }; + return { + status: "fail", + result, + errors, + }; + }; +} + +export function undefinedIfEmpty<T extends object>(obj: T): T | undefined { + return Object.keys(obj).some( + (k) => (obj as Record<string, T>)[k] !== undefined, + ) + ? obj + : undefined; +} + +export function CreateAccount(): VNode { + const { i18n } = useTranslationContext(); + const [settings] = usePreferences(); + const officer = useOfficer(); + + const [notification, withErrorHandler] = useLocalNotificationHandler(); + + const [form, status] = useFormState<FormType>( + [".password", ".repeat"] as Array<UIHandlerId>, + { + password: undefined, + repeat: undefined, + }, + createFormValidator(i18n, settings.allowInsecurePassword), + ); + + const createAccountHandler = + status.status === "fail" || officer.state !== "not-found" + ? undefined + : withErrorHandler( + async () => officer.create(form.password!.value!), + () => {}, + ); + return ( + <div class="flex min-h-full flex-col "> + <LocalNotificationBanner notification={notification} /> + + <div class="sm:mx-auto sm:w-full sm:max-w-md"> + <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> + <i18n.Translate>Create account</i18n.Translate> + </h2> + </div> + + <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> + <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> + <form + class="space-y-6" + noValidate + onSubmit={(e) => { + e.preventDefault(); + }} + autoCapitalize="none" + autoCorrect="off" + > + <div class="mt-2"> + <InputLine<FormType, "password"> + label={i18n.str`Password`} + name="password" + type="password" + required + handler={form.password} + /> + </div> + + <div class="mt-2"> + <InputLine<FormType, "repeat"> + label={i18n.str`Repeat password`} + name="repeat" + type="password" + required + handler={form.repeat} + /> + </div> + + <div class="mt-8"> + <Button + type="submit" + disabled={!createAccountHandler} + class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + handler={createAccountHandler} + > + <i18n.Translate>Create</i18n.Translate> + </Button> + </div> + </form> + </div> + </div> + </div> + ); +} + +/** + * Show the element when the load ended + * @param element + */ +export function doAutoFocus(element: HTMLElement | null) { + if (element) { + setTimeout(() => { + element.focus({ preventScroll: true }); + element.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + }, 100); + } +} |