summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/pages/LoginForm.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/pages/LoginForm.tsx')
-rw-r--r--packages/bank-ui/src/pages/LoginForm.tsx230
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>
+ );
+}