Skip to content

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

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