flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

95 lines3,711 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/** A person's security log, newest first, for its own settings page. */
38export async function loadSecurityLog(user: User): Promise<SecurityEvent[]> {
39 const log = await accounts.securityLog(user).catch(() => null);
40 return log?.ok ? log.value : [];
41}
42
43/** The proof a change carries: this session, and the password if it was just typed. */
44function proof(request: Request, form: FormData): Reauth {
45 const password = String(form.get("password") ?? "");
46 return { sessionToken: sessionTokenOf(request), password: password || null, client: clientOf(request) };
47}
48
49function answer(result: Result<unknown>, form: FormData, notice?: string): EmailActionData {
50 if (result.ok) return notice ? { emailNotice: notice } : null;
51 if (result.error.code === "reauth_required") {
52 return { reauth: { intent: String(form.get("intent")), fields: pendingFields(form), message: result.error.message } };
53 }
54 return { emailError: result.error.message };
55}
56
57/** Handles the Emails section's intents; undefined for any other intent. */
58export async function emailAction(user: User, form: FormData, request: Request): Promise<EmailActionData | undefined> {
59 const email = String(form.get("email") ?? "").trim();
60 switch (form.get("intent")) {
61 case "add-email":
62 return answer(
63 await accounts.addEmail(user, email, proof(request, form)),
64 form,
65 `A confirmation link is on its way to ${email}.`,
66 );
67 case "remove-email":
68 return answer(await accounts.removeEmail(user, email, proof(request, form)), form, `Removed ${email}.`);
69 case "resend-email":
70 return answer(await accounts.resendEmailVerification(user, email), form, `Sent the link to ${email} again.`);
71 case "primary-email":
72 return answer(
73 await accounts.updateEmailSettings(user, { primary: email }, proof(request, form)),
74 form,
75 `${email} is now your primary address.`,
76 );
77 case "backup-email":
78 return answer(
79 await accounts.updateEmailSettings(user, { backup: String(form.get("backup") ?? "") }, proof(request, form)),
80 form,
81 "Saved where security notices go.",
82 );
83 case "email-privacy":
84 return answer(
85 await accounts.updateEmailSettings(
86 user,
87 { privateEmail: form.get("private") === "on", blockPrivatePushes: form.get("block") === "on" },
88 proof(request, form),
89 ),
90 form,
91 "Saved.",
92 );
93 }
94 return undefined;
95}