/** * Renders the sign-in page, which also requests password reset links. * * @module */ import { useState } from "react"; import { useNavigate } from "react-router"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { API_URL } from "../config/api.ts"; import { deserializeAuthResponse } from "../utils/shared/deserialize.ts"; import type { AuthResponse, LoginUserRequest } from "../../shared/user.ts"; import type { Wire } from "../../shared/api.ts"; import { useAuth } from "../hooks/auth/useAuth.ts"; import { useDocumentTitle } from "../hooks/shared/useDocumentTitle.ts"; import { useHeightTransition } from "../hooks/shared/useHeightTransition.ts"; import { PageShell } from "../components/layout/PageShell.tsx"; import { expectOk } from "../utils/shared/api.ts"; import { FormError } from "../components/form/FormError.tsx"; import { FormProvider } from "react-hook-form"; import { SubmitButton } from "../components/form/SubmitButton.tsx"; import { TextField } from "../components/form/TextField.tsx"; import { useApiForm } from "../hooks/form/useApiForm.ts"; interface LoginValues { username: string; password: string; } interface ResetValues { email: string; } /** * Renders `/login`, for the signed out. A sign-in lands on `/`, and "Forgot * password?" unfolds the reset request form below. */ export function UserLogin(): React.JSX.Element { useDocumentTitle(t`Log in`); const navigate = useNavigate(); const { login } = useAuth(); const [showReset, setShowReset] = useState(false); const cardRef = useHeightTransition(showReset); const form = useApiForm({ defaultValues: { username: "", password: "" }, }); const onSubmit = form.submit(async ({ username, password }) => { const data = await expectOk>( await fetch(`${API_URL}/api/sessions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password } satisfies LoginUserRequest), }), ); login(deserializeAuthResponse(data)); navigate("/"); }); return (

Log in

name="username" placeholder={t`Username`} autoFocus rules={{ required: true }} /> name="password" type="password" placeholder={t`Password`} rules={{ required: true }} /> Logging in…}> Log in

{showReset && }

This is a mirage.

); } /** * Renders the form that asks the server to email a password reset link, which * leads to `/reset-password`. */ function ResetRequestPanel() { const [sent, setSent] = useState(false); const form = useApiForm({ defaultValues: { email: "" } }); const onSubmit = form.submit(async ({ email }) => { // The server answers 200 whether or not the address is known, so as not // to reveal which ones are. An error here means it failed. await expectOk( await fetch(`${API_URL}/api/password-resets`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim() }), }), ); setSent(true); }); if (sent) { return (

If that address is registered you'll receive a reset link shortly.

); } return (
name="email" type="email" placeholder={t`Your email address`} autoFocus rules={{ required: true }} /> Sending…}> Send reset link
); }