pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/lib/emails.server.ts

89 lines3,456 bytesCodeBlame
1import type { AccountEmails, Reauth, Result, SecurityEvent, User } from "@g1t/contracts";
2
3import { pendingFields } from "./emails";
4import { accounts } from "./services.server";
5import { clientOf, sessionTokenOf } from "./session.server";
6
7/** A change that waits on the person proving it is them. */
8export type PendingChange = {
9 intent: string;
10 /** The form's other fields, sent again with the password. */
11 fields: Record<string, string>;
12 message: string;
13};
14
15/** What the Emails section's forms answer. */
16export type EmailActionData = {
17 emailError?: string;
18 emailNotice?: string;
19 reauth?: PendingChange;
20} | null;
21
22/** The intents the Emails section posts. */
23export const EMAIL_INTENTS = ["add-email", "remove-email", "resend-email", "primary-email", "backup-email", "email-privacy"] as const;
24
25/** A person's addresses and security log for their settings. */
26export async function loadEmails(user: User): Promise<{ emails: AccountEmails | null; log: SecurityEvent[] }> {
27 const [emails, log] = await Promise.all([
28 accounts.listEmails(user).catch(() => null),
29 accounts.securityLog(user).catch(() => null),
30 ]);
31 return {
32 emails: emails?.ok ? emails.value : null,
33 log: log?.ok ? log.value : [],
34 };
35}
36
37/** The proof a change carries: this session, and the password if it was just typed. */
38function proof(request: Request, form: FormData): Reauth {
39 const password = String(form.get("password") ?? "");
40 return { sessionToken: sessionTokenOf(request), password: password || null, client: clientOf(request) };
41}
42
43function answer(result: Result<unknown>, form: FormData, notice?: string): EmailActionData {
44 if (result.ok) return notice ? { emailNotice: notice } : null;
45 if (result.error.code === "reauth_required") {
46 return { reauth: { intent: String(form.get("intent")), fields: pendingFields(form), message: result.error.message } };
47 }
48 return { emailError: result.error.message };
49}
50
51/** Handles the Emails section's intents; undefined for any other intent. */
52export async function emailAction(user: User, form: FormData, request: Request): Promise<EmailActionData | undefined> {
53 const email = String(form.get("email") ?? "").trim();
54 switch (form.get("intent")) {
55 case "add-email":
56 return answer(
57 await accounts.addEmail(user, email, proof(request, form)),
58 form,
59 `A confirmation link is on its way to ${email}.`,
60 );
61 case "remove-email":
62 return answer(await accounts.removeEmail(user, email, proof(request, form)), form, `Removed ${email}.`);
63 case "resend-email":
64 return answer(await accounts.resendEmailVerification(user, email), form, `Sent the link to ${email} again.`);
65 case "primary-email":
66 return answer(
67 await accounts.updateEmailSettings(user, { primary: email }, proof(request, form)),
68 form,
69 `${email} is now your primary address.`,
70 );
71 case "backup-email":
72 return answer(
73 await accounts.updateEmailSettings(user, { backup: String(form.get("backup") ?? "") }, proof(request, form)),
74 form,
75 "Saved where security notices go.",
76 );
77 case "email-privacy":
78 return answer(
79 await accounts.updateEmailSettings(
80 user,
81 { privateEmail: form.get("private") === "on", blockPrivatePushes: form.get("block") === "on" },
82 proof(request, form),
83 ),
84 form,
85 "Saved.",
86 );
87 }
88 return undefined;
89}