Commit

Forgot password and OAuth approval: a service failure is said in the form with a safe message, logged, never shown as the error page

syntaqxcommitted Parent0f8031aBrowse files
3 files+28−110/3 viewed
+3−1
554554 yet can use that address, and following the link confirms it.
555555
556556 The page answers the same way whether or not the address has an account.
557−g1t sends at most 5 reset links an hour to one address.
557+g1t sends at most 5 reset links an hour to one address. If g1t cannot
558+take the request at all, the page says so and keeps what you typed, so you
559+can try again.
558560
559561 Setting a new password signs you out everywhere and emails your primary
560562 and backup addresses.
+11−3
33 import type { Route } from "./+types/forgot";
44 import { page } from "../lib/meta";
55 import { AuthCard } from "../components/auth-card";
6−import { Field, Input, SubmitButton } from "../components/ui";
6+import { ErrorText, Field, Input, SubmitButton } from "../components/ui";
77 import { identity } from "../lib/services.server";
88 import { assertSameOrigin, clientOf } from "../lib/session.server";
99
1414 export async function action({ request }: Route.ActionArgs) {
1515 assertSameOrigin(request);
1616 const form = await request.formData();
17− await identity.requestPasswordReset(String(form.get("email") ?? ""), clientOf(request));
18− return { sent: true };
17+ try {
18+ await identity.requestPasswordReset(String(form.get("email") ?? ""), clientOf(request));
19+ } catch (error) {
20+ // Logged, never shown: what went wrong could say whether the address
21+ // has an account. The person hears only that it did not go.
22+ console.warn("forgot:", error);
23+ return { sent: false, error: "We could not send a reset link just now. Try again in a few minutes." };
24+ }
25+ return { sent: true, error: null };
1926 }
2027
2128 export default function Forgot({ actionData }: Route.ComponentProps) {
3946 <Field label="Email">
4047 <Input name="email" type="email" autoComplete="email" required autoFocus />
4148 </Field>
49+ <ErrorText>{actionData?.error}</ErrorText>
4250 <div className="pt-2 *:w-full">
4351 <SubmitButton pending="Sending…">Send reset link</SubmitButton>
4452 </div>
+14−7
103103 if (scopes.length === 0) {
104104 return { error: "Leave at least one box ticked, or deny." };
105105 }
106− const { code } = await identity.oauthAuthorize(user, {
107− clientId: checked.clientId,
108− clientName: checked.clientName,
109− redirectUri: checked.redirectUri,
110− codeChallenge: checked.codeChallenge,
111− scopes,
112− });
106+ let code: string;
107+ try {
108+ ({ code } = await identity.oauthAuthorize(user, {
109+ clientId: checked.clientId,
110+ clientName: checked.clientName,
111+ redirectUri: checked.redirectUri,
112+ codeChallenge: checked.codeChallenge,
113+ scopes,
114+ }));
115+ } catch (error) {
116+ // The service's own words are for the log; the person can try again.
117+ console.warn("oauth-authorize:", error);
118+ return { error: "g1t could not approve this sign-in just now. Try again in a moment." };
119+ }
113120 // `iss` is the API's origin, the issuer its metadata names (RFC 9207).
114121 throw redirect(
115122 callback(checked.redirectUri, { code, state: checked.state, iss: addresses().api }),