239 lines
9.1 KiB
TypeScript
239 lines
9.1 KiB
TypeScript
/*
|
|
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 { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
|
import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
|
import { Fragment, VNode, h } from "preact";
|
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
import { useBackendContext } from "../context/backend.js";
|
|
import { bankUiSettings } from "../settings.js";
|
|
import { undefinedIfEmpty } from "../utils.js";
|
|
import { USERNAME_REGEX } from "./RegistrationPage.js";
|
|
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
|
import { AccessToken, useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
|
|
import { useCredentialsCheckerOld } from "../hooks/backend.js";
|
|
|
|
/**
|
|
* Collect and submit login data.
|
|
*/
|
|
export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
|
const backend = useBackendContext();
|
|
const [username, setUsername] = useState<string | undefined>();
|
|
const [password, setPassword] = useState<string | undefined>();
|
|
const { i18n } = useTranslationContext();
|
|
// const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
|
|
|
|
const testLogin = useCredentialsCheckerOld();
|
|
const ref = useRef<HTMLInputElement>(null);
|
|
useEffect(function focusInput() {
|
|
ref.current?.focus();
|
|
}, []);
|
|
const [busy, setBusy] = useState<Record<string, undefined>>()
|
|
|
|
const errors = undefinedIfEmpty({
|
|
username: !username
|
|
? i18n.str`Missing username`
|
|
: !USERNAME_REGEX.test(username)
|
|
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
|
|
: undefined,
|
|
password: !password ? i18n.str`Missing password` : undefined,
|
|
}) ?? busy;
|
|
|
|
function saveError({ title, description, debug }: { title: TranslatedString, description?: TranslatedString, debug?: any }) {
|
|
notifyError(title, description, debug)
|
|
}
|
|
|
|
async function doLogin() {
|
|
if (!username || !password) return;
|
|
setBusy({})
|
|
const testResult = await testLogin(username, password);
|
|
if (testResult.valid) {
|
|
backend.logIn({ username, password });
|
|
} else {
|
|
if (testResult.requestError) {
|
|
const { cause } = testResult;
|
|
switch (cause.type) {
|
|
case ErrorType.CLIENT: {
|
|
if (cause.status === HttpStatusCode.Unauthorized) {
|
|
saveError({
|
|
title: i18n.str`Wrong credentials for "${username}"`,
|
|
});
|
|
} else
|
|
if (cause.status === HttpStatusCode.NotFound) {
|
|
saveError({
|
|
title: i18n.str`Account not found`,
|
|
});
|
|
} else {
|
|
saveError({
|
|
title: i18n.str`Could not load due to a client error`,
|
|
description: cause.payload.error.description,
|
|
debug: JSON.stringify(cause.payload),
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case ErrorType.SERVER: {
|
|
saveError({
|
|
title: i18n.str`Server had a problem, try again later or report.`,
|
|
description: cause.payload.error.description,
|
|
debug: JSON.stringify(cause.payload),
|
|
});
|
|
break;
|
|
}
|
|
case ErrorType.TIMEOUT: {
|
|
saveError({
|
|
title: i18n.str`Request timeout, try again later.`,
|
|
});
|
|
break;
|
|
}
|
|
case ErrorType.UNREADABLE: {
|
|
saveError({
|
|
title: i18n.str`Unexpected error.`,
|
|
description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString,
|
|
debug: JSON.stringify(cause),
|
|
});
|
|
break;
|
|
}
|
|
default: {
|
|
saveError({
|
|
title: i18n.str`Unexpected error, please report.`,
|
|
description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString,
|
|
debug: JSON.stringify(cause),
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
saveError({
|
|
title: i18n.str`Unexpected error, please report.`,
|
|
debug: JSON.stringify(testResult.error),
|
|
});
|
|
}
|
|
backend.logOut();
|
|
}
|
|
setPassword(undefined);
|
|
setBusy(undefined)
|
|
}
|
|
|
|
return (
|
|
<Fragment>
|
|
<h1 class="nav"></h1>
|
|
{/* {error && (
|
|
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
|
|
)} */}
|
|
|
|
<div class="flex min-h-full flex-col justify-center">
|
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
|
<h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2>
|
|
</div>
|
|
|
|
<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={ref}
|
|
autoFocus
|
|
type="text"
|
|
name="username"
|
|
id="username"
|
|
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"
|
|
value={username ?? ""}
|
|
enterkeyhint="next"
|
|
placeholder="identification"
|
|
autocomplete="username"
|
|
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">Password</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"
|
|
required
|
|
onInput={(e): void => {
|
|
setPassword(e.currentTarget.value);
|
|
}}
|
|
/>
|
|
<ShowInputErrorLabel
|
|
message={errors?.password}
|
|
isDirty={password !== undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<button type="submit"
|
|
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}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
doLogin()
|
|
}}
|
|
>
|
|
<i18n.Translate>Log in</i18n.Translate>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
{bankUiSettings.allowRegistrations && onRegister &&
|
|
<p class="mt-10 text-center text-sm text-gray-500 border-t">
|
|
<button type="submit"
|
|
class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
onRegister()
|
|
}}
|
|
>
|
|
<i18n.Translate>Register</i18n.Translate>
|
|
</button>
|
|
</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</Fragment>
|
|
);
|
|
}
|