auth-usernames.ts76 lines · main
1/**
2 * Username authentication — Phase 7.3.
3 *
4 * Users can set a unique username and use it instead of their email
5 * to sign in. The username resolves to the user's email internally,
6 * then the normal Better Auth email/password flow is used.
7 */
8
9import { ValidationError } from '@briven/shared';
10
11import { runInProjectDatabase } from '../db/data-plane.js';
12
13const USERNAME_RE = /^[a-zA-Z0-9_-]{3,32}$/;
14
15export function validateUsername(username: string): void {
16 if (!USERNAME_RE.test(username)) {
17 throw new ValidationError(
18 'username must be 3-32 characters and contain only letters, numbers, underscores, and hyphens',
19 );
20 }
21}
22
23export async function createUsername(
24 projectId: string,
25 userId: string,
26 username: string,
27): Promise<void> {
28 validateUsername(username);
29 const id = crypto.randomUUID();
30 await runInProjectDatabase(projectId, async (tx) => {
31 await tx.unsafe(
32 `INSERT INTO "_briven_auth_user_usernames" (id, user_id, username) VALUES ($1, $2, $3) ON CONFLICT (user_id) DO UPDATE SET username = $3, updated_at = now()`,
33 [id, userId, username] as never,
34 );
35 });
36}
37
38export async function deleteUsername(projectId: string, userId: string): Promise<void> {
39 await runInProjectDatabase(projectId, async (tx) => {
40 await tx.unsafe(
41 `DELETE FROM "_briven_auth_user_usernames" WHERE user_id = $1`,
42 [userId] as never,
43 );
44 });
45}
46
47export async function resolveUsernameToEmail(
48 projectId: string,
49 username: string,
50): Promise<{ userId: string; email: string } | null> {
51 const rows = await runInProjectDatabase(projectId, async (tx) => {
52 return (await tx.unsafe(
53 `SELECT u.id, u.email
54 FROM "_briven_auth_users" u
55 JOIN "_briven_auth_user_usernames" n ON n.user_id = u.id
56 WHERE n.username = $1
57 LIMIT 1`,
58 [username] as never,
59 )) as Array<{ id: string; email: string }>;
60 });
61 if (!rows[0]) return null;
62 return { userId: rows[0].id, email: rows[0].email };
63}
64
65export async function getUsernameByUserId(
66 projectId: string,
67 userId: string,
68): Promise<string | null> {
69 const rows = await runInProjectDatabase(projectId, async (tx) => {
70 return (await tx.unsafe(
71 `SELECT username FROM "_briven_auth_user_usernames" WHERE user_id = $1 LIMIT 1`,
72 [userId] as never,
73 )) as Array<{ username: string }>;
74 });
75 return rows[0]?.username ?? null;
76}