~james/core

core/web/src/lib/api/emails.ts -rw-r--r-- 3.4 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import { authedGet, authedPost, type DeliberiContext } from "#lib/api/deliberi.ts";
import { buildUrl } from "#lib/api/_request.ts";
import { ClientResponseError } from "#lib/api/client.ts";
import { nsidOf } from "#lib/api/syntax.ts";
import type { Fetch } from "#lib/fetch.ts";

export interface ListEmailItem {
	address: string;
	verified: boolean;
	primary: boolean;
	createdAt: string;
}

const LIST_EMAILS = nsidOf("org.tangled.temp.account.listEmails");
const ADD_EMAIL = nsidOf("org.tangled.temp.account.addEmail");
const VERIFY_EMAIL = nsidOf("org.tangled.temp.account.verifyEmail");
const DELETE_EMAIL = nsidOf("org.tangled.temp.account.deleteEmail");
const SET_PRIMARY_EMAIL = nsidOf("org.tangled.temp.account.setPrimaryEmail");

export const listEmails = (ctx: DeliberiContext): Promise<{ emails: ListEmailItem[] }> =>
	authedGet(ctx, LIST_EMAILS);

export const addEmail = (ctx: DeliberiContext, email: string): Promise<void> =>
	authedPost(ctx, ADD_EMAIL, { email }).then(() => undefined);

export class VerificationError extends Error {
	constructor(
		message: string,
		readonly kind: "invalid" | "unavailable"
	) {
		super(message);
	}
}

export const verifyEmailByToken = async (
	serviceUrl: string,
	token: string,
	fetchFn: Fetch = fetch
): Promise<{ email: string }> => {
	const response = await fetchFn(buildUrl(serviceUrl, VERIFY_EMAIL), {
		method: "POST",
		headers: { "content-type": "application/json", accept: "application/json" },
		body: JSON.stringify({ token })
	});
	if (response.status === 400) {
		throw new VerificationError(
			"The verification link is invalid or has already been used.",
			"invalid"
		);
	}
	if (!response.ok) {
		throw new VerificationError(
			`Could not reach the verification service (${response.status}).`,
			"unavailable"
		);
	}
	const text = await response.text();
	return text ? JSON.parse(text) : { email: "" };
};

export const emailServiceError = (cause: unknown): string => {
	if (cause instanceof ClientResponseError) {
		if (cause.status === 401 || cause.status === 403) {
			return "Unable to confirm your session with the email service. Sign out, sign back in, and try again.";
		}
		if (cause.status >= 500) return "Unable to reach the email service. Try again in a moment.";
		if (cause.error === "InvalidEmail") {
			return "Check the email address. It needs a domain that can receive mail.";
		}
		const description = cause.description?.trim();
		if (!description) return "Unable to update your emails. Try again.";
		const sentence = description.charAt(0).toUpperCase() + description.slice(1);
		return /[.!?]$/.test(sentence) ? sentence : `${sentence}.`;
	}
	if (cause instanceof TypeError) {
		return "Unable to reach the email service. Check your connection and try again.";
	}
	return cause instanceof Error ? cause.message : String(cause);
};

export const deleteEmail = (ctx: DeliberiContext, email: string): Promise<void> =>
	authedPost(ctx, DELETE_EMAIL, { email }).then(() => undefined);

export const setPrimaryEmail = (ctx: DeliberiContext, email: string): Promise<void> =>
	authedPost(ctx, SET_PRIMARY_EMAIL, { email }).then(() => undefined);

export const markPrimary = (rows: ListEmailItem[], address: string): ListEmailItem[] =>
	rows.map((row) => ({ ...row, primary: row.address === address }));

export const removeEmailRow = (rows: ListEmailItem[], address: string): ListEmailItem[] =>
	rows.filter((row) => row.address !== address);