返回 DeepSeek-Reasonix
emailTokens.ts
根目录 / workers / accounts / src / db / emailTokens.ts
1 import { generateToken, hashToken } from "../auth/crypto";
2
3 export type EmailTokenPurpose = "verify" | "reset";
4
5 export class EmailTokenRepo {
6 constructor(
7 private readonly db: D1Database,
8 private readonly pepper: string,
9 ) {}
10
11 // Issues a single-use token and returns the raw value for the email link.
12 async issue(userId: number, purpose: EmailTokenPurpose, ttlMs: number): Promise<string> {
13 const token = generateToken();
14 const tokenHash = await hashToken(this.pepper, token);
15 const now = new Date();
16 const expires = new Date(now.getTime() + ttlMs);
17 await this.db
18 .prepare("INSERT INTO email_tokens (token_hash, user_id, purpose, created_at, expires_at) VALUES (?1, ?2, ?3, ?4, ?5)")
19 .bind(tokenHash, userId, purpose, now.toISOString(), expires.toISOString())
20 .run();
21 return token;
22 }
23
24 // Atomically redeems a token: a single UPDATE ... RETURNING marks it used and
25 // hands back the user id, so a token can never be consumed twice.
26 async consume(token: string, purpose: EmailTokenPurpose): Promise<number | null> {
27 const tokenHash = await hashToken(this.pepper, token);
28 const now = new Date().toISOString();
29 const row = await this.db
30 .prepare(
31 `UPDATE email_tokens SET used_at = ?1
32 WHERE token_hash = ?2 AND purpose = ?3 AND used_at IS NULL AND expires_at > ?1
33 RETURNING user_id`,
34 )
35 .bind(now, tokenHash, purpose)
36 .first<{ user_id: number }>();
37 return row?.user_id ?? null;
38 }
39
40 async invalidateForUser(userId: number, purpose: EmailTokenPurpose): Promise<void> {
41 await this.db
42 .prepare("UPDATE email_tokens SET used_at = ?1 WHERE user_id = ?2 AND purpose = ?3 AND used_at IS NULL")
43 .bind(new Date().toISOString(), userId, purpose)
44 .run();
45 }
46 }
47
47 lines TYPESCRIPT