Review an email unsubscribe endpoint

src/unsubscribe-handler.tsTypeScript
@@ -0,0 +1,47 @@
1+export interface UnsubscribeRequest {
2+ method: string;
3+ token: string;
4+}
5+
6+export interface UnsubscribeResponse {
7+ status: number;
8+ body?: string;
9+}
10+
11+interface TokenPayload {
12+ email: string;
13+ expiresAtMs: number;
14+ signature: string;
15+}
16+
17+export interface UnsubscribeDependencies {
18+ clock: { now(): number };
19+ decoder: { decode(token: string): TokenPayload };
20+ signer: {
21+ sign(email: string, expiresAtMs: number): string;
22+ verify(email: string, expiresAtMs: number, signature: string): boolean;
23+ };
24+ subscriptions: { unsubscribeMarketing(email: string): Promise<void> };
25+}
26+
27+export async function handleUnsubscribe(
28+ request: UnsubscribeRequest,
29+ dependencies: UnsubscribeDependencies,
30+): Promise<UnsubscribeResponse> {
31+ const payload = dependencies.decoder.decode(request.token);
32+
33+ if (dependencies.clock.now() > payload.expiresAtMs) {
34+ return { status: 400, body: "Invalid unsubscribe link" };
35+ }
36+
37+ const expected = dependencies.signer.sign(
38+ payload.email,
39+ payload.expiresAtMs,
40+ );
41+ if (expected !== payload.signature) {
42+ return { status: 400, body: "Invalid unsubscribe link" };
43+ }
44+
45+ await dependencies.subscriptions.unsubscribeMarketing(payload.email);
46+ return { status: 204 };
47+}