~james/core

core/web/src/lib/api/account.ts -rw-r--r-- 4.0 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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { Client, ClientResponseError, ok, simpleFetchHandler } from "@atcute/client";
import { mainSchema as createSessionSchema } from "@atcute/atproto/types/server/createSession";
import { mainSchema as deleteAccountSchema } from "@atcute/atproto/types/server/deleteAccount";
import { mainSchema as deleteSessionSchema } from "@atcute/atproto/types/server/deleteSession";
import { mainSchema as requestAccountDeleteSchema } from "@atcute/atproto/types/server/requestAccountDelete";
import { mainSchema as requestPasswordResetSchema } from "@atcute/atproto/types/server/requestPasswordReset";
import { mainSchema as resetPasswordSchema } from "@atcute/atproto/types/server/resetPassword";
import type { Did } from "@atcute/lexicons/syntax";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import { sentence } from "#lib/format.ts";

const pdsClient = (service: string, token?: string): Client =>
	new Client({
		handler: simpleFetchHandler({
			service,
			fetch: (token
				? (input, init) => {
						const headers = new Headers(init?.headers);
						headers.set("authorization", `Bearer ${token}`);
						return fetch(input, { ...init, headers });
					}
				: fetch) as typeof fetch
		})
	});

const accountError = (cause: ClientResponseError, fallback: string): string => {
	if (cause.error === "AuthenticationRequired" || cause.error === "AuthenticationFailed") {
		return "Password is incorrect.";
	}
	if (cause.error === "AuthFactorTokenRequired") {
		return "Your account uses two-factor sign in, which this page does not support yet.";
	}
	if (cause.error === "InvalidToken" || cause.error === "ExpiredToken") {
		return "The code is invalid or has expired. Send a new code and try again.";
	}
	if (cause.description) return sentence(cause.description);
	return fallback;
};

const guard = async <T>(fallback: string, operation: () => Promise<T>): Promise<T> => {
	try {
		return await operation();
	} catch (cause) {
		if (cause instanceof ClientResponseError) {
			throw new Error(accountError(cause, fallback), { cause });
		}
		throw cause;
	}
};

const withPasswordSession = async <T>(
	agent: OAuthUserAgent,
	password: string,
	operation: (client: Client, session: { email?: string }) => Promise<T>
): Promise<T> => {
	const service = agent.session.info.aud;
	const session = await ok(
		pdsClient(service).call(createSessionSchema, {
			input: { identifier: agent.sub, password }
		})
	);
	try {
		return await operation(pdsClient(service, session.accessJwt), session);
	} finally {
		await pdsClient(service, session.refreshJwt)
			.call(deleteSessionSchema)
			.catch(() => undefined);
	}
};

export const requestAccountDeletion = (agent: OAuthUserAgent, password: string): Promise<void> =>
	guard("Could not send a deletion code. Try again.", () =>
		withPasswordSession(agent, password, async (client) => {
			await ok(client.call(requestAccountDeleteSchema));
		})
	);

export const deleteAccount = (
	agent: OAuthUserAgent,
	input: { password: string; token: string }
): Promise<Did> =>
	guard("Could not delete your account. Try again.", async () => {
		const did = agent.sub;
		await ok(
			pdsClient(agent.session.info.aud).call(deleteAccountSchema, {
				input: { did, password: input.password, token: input.token.trim() }
			})
		);
		return did;
	});

export const requestPasswordReset = (agent: OAuthUserAgent, password: string): Promise<void> =>
	guard("Could not send a reset code. Try again.", () =>
		withPasswordSession(agent, password, async (_client, { email }) => {
			if (!email) throw new Error("Your account has no email address to send a code to.");
			await ok(
				pdsClient(agent.session.info.aud).call(requestPasswordResetSchema, {
					input: { email }
				})
			);
		})
	);

export const resetPassword = (
	agent: OAuthUserAgent,
	input: { token: string; password: string }
): Promise<void> =>
	guard("Could not change your password. Try again.", async () => {
		await ok(
			pdsClient(agent.session.info.aud).call(resetPasswordSchema, {
				input: { token: input.token.trim(), password: input.password }
			})
		);
	});