auth-team-seats.ts122 lines · main
1/**
2 * Auth dashboard team seats (Phase 6.1).
3 *
4 * Project owners can invite team members to manage auth settings without
5 * granting full project admin access. Auth team members receive `admin` or
6 * `viewer` roles that are checked by `requireAuthTeamAdmin` middleware on
7 * auth-service routes.
8 */
9
10import { eq, and } from 'drizzle-orm';
11import { getDb } from '../db/client.js';
12import { projectAuthTeamMembers, users, authTeamRole, type AuthTeamRole } from '../db/schema.js';
13import { NotFoundError, ValidationError } from '@briven/shared';
14
15export interface AuthTeamMember {
16 projectId: string;
17 userId: string;
18 role: AuthTeamRole;
19 email: string;
20 name: string | null;
21 invitedBy: string | null;
22 createdAt: Date;
23}
24
25export async function listAuthTeamMembers(projectId: string): Promise<AuthTeamMember[]> {
26 const db = getDb();
27 const rows = await db
28 .select({
29 projectId: projectAuthTeamMembers.projectId,
30 userId: projectAuthTeamMembers.userId,
31 role: projectAuthTeamMembers.role,
32 email: users.email,
33 name: users.name,
34 invitedBy: projectAuthTeamMembers.invitedBy,
35 createdAt: projectAuthTeamMembers.createdAt,
36 })
37 .from(projectAuthTeamMembers)
38 .innerJoin(users, eq(users.id, projectAuthTeamMembers.userId))
39 .where(eq(projectAuthTeamMembers.projectId, projectId))
40 .orderBy(projectAuthTeamMembers.createdAt);
41
42 return rows.map((r) => ({
43 projectId: r.projectId,
44 userId: r.userId,
45 role: r.role as AuthTeamRole,
46 email: r.email,
47 name: r.name,
48 invitedBy: r.invitedBy,
49 createdAt: r.createdAt,
50 }));
51}
52
53export async function getAuthTeamRole(
54 projectId: string,
55 userId: string,
56): Promise<AuthTeamRole | null> {
57 const db = getDb();
58 const [row] = await db
59 .select({ role: projectAuthTeamMembers.role })
60 .from(projectAuthTeamMembers)
61 .where(
62 and(
63 eq(projectAuthTeamMembers.projectId, projectId),
64 eq(projectAuthTeamMembers.userId, userId),
65 ),
66 )
67 .limit(1);
68 return (row?.role as AuthTeamRole) ?? null;
69}
70
71export async function addAuthTeamMember(input: {
72 projectId: string;
73 userId: string;
74 role: AuthTeamRole;
75 invitedBy: string;
76}): Promise<{ projectId: string; userId: string; role: AuthTeamRole }> {
77 if (!authTeamRole.includes(input.role)) {
78 throw new ValidationError('role must be admin or viewer', { role: input.role });
79 }
80 const db = getDb();
81 await db
82 .insert(projectAuthTeamMembers)
83 .values({
84 projectId: input.projectId,
85 userId: input.userId,
86 role: input.role,
87 invitedBy: input.invitedBy,
88 })
89 .onConflictDoNothing({
90 target: [projectAuthTeamMembers.projectId, projectAuthTeamMembers.userId],
91 });
92 return { projectId: input.projectId, userId: input.userId, role: input.role };
93}
94
95export async function removeAuthTeamMember(
96 projectId: string,
97 userId: string,
98): Promise<void> {
99 const db = getDb();
100 const result = await db
101 .delete(projectAuthTeamMembers)
102 .where(
103 and(
104 eq(projectAuthTeamMembers.projectId, projectId),
105 eq(projectAuthTeamMembers.userId, userId),
106 ),
107 )
108 .returning({ projectId: projectAuthTeamMembers.projectId });
109 if (!result[0]) {
110 throw new NotFoundError('auth_team_member', `${projectId}:${userId}`);
111 }
112}
113
114export async function findUserByEmail(email: string): Promise<{ id: string; email: string } | null> {
115 const db = getDb();
116 const [row] = await db
117 .select({ id: users.id, email: users.email })
118 .from(users)
119 .where(eq(users.email, email))
120 .limit(1);
121 return row ?? null;
122}