diff options
Diffstat (limited to 'packages/bank-ui/src/pages/LoginForm.tsx')
-rw-r--r-- | packages/bank-ui/src/pages/LoginForm.tsx | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx new file mode 100644 index 000000000..2f967895c --- /dev/null +++ b/packages/bank-ui/src/pages/LoginForm.tsx @@ -0,0 +1,230 @@ +/* + 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 { HttpStatusCode } from "@gnu-taler/taler-util"; +import { + Button, + LocalNotificationBanner, + ShowInputErrorLabel, + useLocalNotificationHandler, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { useEffect, useRef, useState } from "preact/hooks"; +import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; +import { useSessionState } from "../hooks/session.js"; +import { RouteDefinition } from "@gnu-taler/web-util/browser"; +import { undefinedIfEmpty } from "../utils.js"; +import { doAutoFocus } from "./PaytoWireTransferForm.js"; +import { USERNAME_REGEX } from "./RegistrationPage.js"; + +/** + * Collect and submit login data. + */ +export function LoginForm({ + currentUser, + fixedUser, + routeRegister, +}: { + fixedUser?: boolean; + currentUser?: string; + routeRegister?: RouteDefinition; +}): VNode { + const session = useSessionState(); + + const sessionUser = + session.state.status !== "loggedOut" ? session.state.username : undefined; + const [username, setUsername] = useState<string | undefined>( + currentUser ?? sessionUser, + ); + const [password, setPassword] = useState<string | undefined>(); + const { i18n } = useTranslationContext(); + const { + lib: { auth: authenticator }, + } = useBankCoreApiContext(); + const [notification, withErrorHandler] = useLocalNotificationHandler(); + const { config } = useBankCoreApiContext(); + + const ref = useRef<HTMLInputElement>(null); + useEffect(function focusInput() { + ref.current?.focus(); + }, []); + + const errors = undefinedIfEmpty({ + username: !username + ? i18n.str`Missing username` + : !USERNAME_REGEX.test(username) + ? i18n.str`Use letters, numbers or any of these characters: - . _ ~` + : undefined, + password: !password ? i18n.str`Missing password` : undefined, + }); + + async function doLogout() { + session.logOut(); + } + + const loginHandler = + !username || !password + ? undefined + : withErrorHandler( + async () => + authenticator(username).createAccessTokenBasic(username, password, { + scope: "readwrite", + duration: { d_us: "forever" }, + refreshable: true, + }), + (result) => { + session.logIn({ username, token: result.body.access_token }); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Wrong credentials for "${username}"`; + case HttpStatusCode.NotFound: + return i18n.str`Account not found`; + } + }, + ); + + return ( + <div class="flex min-h-full flex-col justify-center "> + <LocalNotificationBanner notification={notification} /> + <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> + <form + class="space-y-6" + noValidate + onSubmit={(e) => { + e.preventDefault(); + }} + autoCapitalize="none" + autoCorrect="off" + > + <div> + <label + for="username" + class="block text-sm font-medium leading-6 text-gray-900" + > + <i18n.Translate>Username</i18n.Translate> + </label> + <div class="mt-2"> + <input + ref={doAutoFocus} + type="text" + name="username" + id="username" + class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={username ?? ""} + disabled={fixedUser} + enterkeyhint="next" + placeholder="identification" + autocomplete="username" + title={i18n.str`Username of the account`} + required + onInput={(e): void => { + setUsername(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errors?.username} + isDirty={username !== undefined} + /> + </div> + </div> + + <div> + <div class="flex items-center justify-between"> + <label + for="password" + class="block text-sm font-medium leading-6 text-gray-900" + > + <i18n.Translate>Password</i18n.Translate> + </label> + </div> + <div class="mt-2"> + <input + type="password" + name="password" + id="password" + autocomplete="current-password" + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + enterkeyhint="send" + value={password ?? ""} + placeholder="Password" + title={i18n.str`Password of the account`} + required + onInput={(e): void => { + setPassword(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={errors?.password} + isDirty={password !== undefined} + /> + </div> + </div> + + {session.state.status !== "loggedOut" ? ( + <div class="flex justify-between"> + <button + type="submit" + name="cancel" + class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600" + onClick={(e) => { + e.preventDefault(); + doLogout(); + }} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + + <Button + type="submit" + name="check" + class="rounded-md bg-indigo-600 disabled:bg-gray-300 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" + disabled={!!errors} + handler={loginHandler} + > + <i18n.Translate>Check</i18n.Translate> + </Button> + </div> + ) : ( + <div> + <Button + type="submit" + name="login" + class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 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" + disabled={!!errors} + handler={loginHandler} + > + <i18n.Translate>Log in</i18n.Translate> + </Button> + </div> + )} + </form> + + {config.allow_registrations && routeRegister && ( + <a + name="register" + href={routeRegister.url({})} + class="flex justify-center border-t mt-4 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" + > + <i18n.Translate>Register</i18n.Translate> + </a> + )} + </div> + </div> + ); +} |