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 | |
| 10 | import { eq, and } from 'drizzle-orm'; |
| 11 | import { getDb } from '../db/client.js'; |
| 12 | import { projectAuthTeamMembers, users, authTeamRole, type AuthTeamRole } from '../db/schema.js'; |
| 13 | import { NotFoundError, ValidationError } from '@briven/shared'; |
| 14 | |
| 15 | export 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 | |
| 25 | export 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 | |
| 53 | export 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 | |
| 71 | export 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 | |
| 95 | export 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 | |
| 114 | export 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 | } |