Spaces:
Paused
Paused
| import { and, eq, inArray, sql } from "drizzle-orm"; | |
| import type { Db } from "@paperclipai/db"; | |
| import { | |
| companyMemberships, | |
| instanceUserRoles, | |
| principalPermissionGrants, | |
| } from "@paperclipai/db"; | |
| import type { PermissionKey, PrincipalType } from "@paperclipai/shared"; | |
| type MembershipRow = typeof companyMemberships.$inferSelect; | |
| type GrantInput = { | |
| permissionKey: PermissionKey; | |
| scope?: Record<string, unknown> | null; | |
| }; | |
| export function accessService(db: Db) { | |
| async function isInstanceAdmin(userId: string | null | undefined): Promise<boolean> { | |
| if (!userId) return false; | |
| const row = await db | |
| .select({ id: instanceUserRoles.id }) | |
| .from(instanceUserRoles) | |
| .where(and(eq(instanceUserRoles.userId, userId), eq(instanceUserRoles.role, "instance_admin"))) | |
| .then((rows) => rows[0] ?? null); | |
| return Boolean(row); | |
| } | |
| async function getMembership( | |
| companyId: string, | |
| principalType: PrincipalType, | |
| principalId: string, | |
| ): Promise<MembershipRow | null> { | |
| return db | |
| .select() | |
| .from(companyMemberships) | |
| .where( | |
| and( | |
| eq(companyMemberships.companyId, companyId), | |
| eq(companyMemberships.principalType, principalType), | |
| eq(companyMemberships.principalId, principalId), | |
| ), | |
| ) | |
| .then((rows) => rows[0] ?? null); | |
| } | |
| async function hasPermission( | |
| companyId: string, | |
| principalType: PrincipalType, | |
| principalId: string, | |
| permissionKey: PermissionKey, | |
| ): Promise<boolean> { | |
| const membership = await getMembership(companyId, principalType, principalId); | |
| if (!membership || membership.status !== "active") return false; | |
| const grant = await db | |
| .select({ id: principalPermissionGrants.id }) | |
| .from(principalPermissionGrants) | |
| .where( | |
| and( | |
| eq(principalPermissionGrants.companyId, companyId), | |
| eq(principalPermissionGrants.principalType, principalType), | |
| eq(principalPermissionGrants.principalId, principalId), | |
| eq(principalPermissionGrants.permissionKey, permissionKey), | |
| ), | |
| ) | |
| .then((rows) => rows[0] ?? null); | |
| return Boolean(grant); | |
| } | |
| async function canUser( | |
| companyId: string, | |
| userId: string | null | undefined, | |
| permissionKey: PermissionKey, | |
| ): Promise<boolean> { | |
| if (!userId) return false; | |
| if (await isInstanceAdmin(userId)) return true; | |
| return hasPermission(companyId, "user", userId, permissionKey); | |
| } | |
| async function listMembers(companyId: string) { | |
| return db | |
| .select() | |
| .from(companyMemberships) | |
| .where(eq(companyMemberships.companyId, companyId)) | |
| .orderBy(sql`${companyMemberships.createdAt} desc`); | |
| } | |
| async function setMemberPermissions( | |
| companyId: string, | |
| memberId: string, | |
| grants: GrantInput[], | |
| grantedByUserId: string | null, | |
| ) { | |
| const member = await db | |
| .select() | |
| .from(companyMemberships) | |
| .where(and(eq(companyMemberships.companyId, companyId), eq(companyMemberships.id, memberId))) | |
| .then((rows) => rows[0] ?? null); | |
| if (!member) return null; | |
| await db.transaction(async (tx) => { | |
| await tx | |
| .delete(principalPermissionGrants) | |
| .where( | |
| and( | |
| eq(principalPermissionGrants.companyId, companyId), | |
| eq(principalPermissionGrants.principalType, member.principalType), | |
| eq(principalPermissionGrants.principalId, member.principalId), | |
| ), | |
| ); | |
| if (grants.length > 0) { | |
| await tx.insert(principalPermissionGrants).values( | |
| grants.map((grant) => ({ | |
| companyId, | |
| principalType: member.principalType, | |
| principalId: member.principalId, | |
| permissionKey: grant.permissionKey, | |
| scope: grant.scope ?? null, | |
| grantedByUserId, | |
| createdAt: new Date(), | |
| updatedAt: new Date(), | |
| })), | |
| ); | |
| } | |
| }); | |
| return member; | |
| } | |
| async function promoteInstanceAdmin(userId: string) { | |
| const existing = await db | |
| .select() | |
| .from(instanceUserRoles) | |
| .where(and(eq(instanceUserRoles.userId, userId), eq(instanceUserRoles.role, "instance_admin"))) | |
| .then((rows) => rows[0] ?? null); | |
| if (existing) return existing; | |
| return db | |
| .insert(instanceUserRoles) | |
| .values({ | |
| userId, | |
| role: "instance_admin", | |
| }) | |
| .returning() | |
| .then((rows) => rows[0]); | |
| } | |
| async function demoteInstanceAdmin(userId: string) { | |
| return db | |
| .delete(instanceUserRoles) | |
| .where(and(eq(instanceUserRoles.userId, userId), eq(instanceUserRoles.role, "instance_admin"))) | |
| .returning() | |
| .then((rows) => rows[0] ?? null); | |
| } | |
| async function listUserCompanyAccess(userId: string) { | |
| return db | |
| .select() | |
| .from(companyMemberships) | |
| .where(and(eq(companyMemberships.principalType, "user"), eq(companyMemberships.principalId, userId))) | |
| .orderBy(sql`${companyMemberships.createdAt} desc`); | |
| } | |
| async function setUserCompanyAccess(userId: string, companyIds: string[]) { | |
| const existing = await listUserCompanyAccess(userId); | |
| const existingByCompany = new Map(existing.map((row) => [row.companyId, row])); | |
| const target = new Set(companyIds); | |
| await db.transaction(async (tx) => { | |
| const toDelete = existing.filter((row) => !target.has(row.companyId)).map((row) => row.id); | |
| if (toDelete.length > 0) { | |
| await tx.delete(companyMemberships).where(inArray(companyMemberships.id, toDelete)); | |
| } | |
| for (const companyId of target) { | |
| if (existingByCompany.has(companyId)) continue; | |
| await tx.insert(companyMemberships).values({ | |
| companyId, | |
| principalType: "user", | |
| principalId: userId, | |
| status: "active", | |
| membershipRole: "member", | |
| }); | |
| } | |
| }); | |
| return listUserCompanyAccess(userId); | |
| } | |
| async function ensureMembership( | |
| companyId: string, | |
| principalType: PrincipalType, | |
| principalId: string, | |
| membershipRole: string | null = "member", | |
| status: "pending" | "active" | "suspended" = "active", | |
| ) { | |
| const existing = await getMembership(companyId, principalType, principalId); | |
| if (existing) { | |
| if (existing.status !== status || existing.membershipRole !== membershipRole) { | |
| const updated = await db | |
| .update(companyMemberships) | |
| .set({ status, membershipRole, updatedAt: new Date() }) | |
| .where(eq(companyMemberships.id, existing.id)) | |
| .returning() | |
| .then((rows) => rows[0] ?? null); | |
| return updated ?? existing; | |
| } | |
| return existing; | |
| } | |
| return db | |
| .insert(companyMemberships) | |
| .values({ | |
| companyId, | |
| principalType, | |
| principalId, | |
| status, | |
| membershipRole, | |
| }) | |
| .returning() | |
| .then((rows) => rows[0]); | |
| } | |
| async function setPrincipalGrants( | |
| companyId: string, | |
| principalType: PrincipalType, | |
| principalId: string, | |
| grants: GrantInput[], | |
| grantedByUserId: string | null, | |
| ) { | |
| await db.transaction(async (tx) => { | |
| await tx | |
| .delete(principalPermissionGrants) | |
| .where( | |
| and( | |
| eq(principalPermissionGrants.companyId, companyId), | |
| eq(principalPermissionGrants.principalType, principalType), | |
| eq(principalPermissionGrants.principalId, principalId), | |
| ), | |
| ); | |
| if (grants.length === 0) return; | |
| await tx.insert(principalPermissionGrants).values( | |
| grants.map((grant) => ({ | |
| companyId, | |
| principalType, | |
| principalId, | |
| permissionKey: grant.permissionKey, | |
| scope: grant.scope ?? null, | |
| grantedByUserId, | |
| createdAt: new Date(), | |
| updatedAt: new Date(), | |
| })), | |
| ); | |
| }); | |
| } | |
| return { | |
| isInstanceAdmin, | |
| canUser, | |
| hasPermission, | |
| getMembership, | |
| ensureMembership, | |
| listMembers, | |
| setMemberPermissions, | |
| promoteInstanceAdmin, | |
| demoteInstanceAdmin, | |
| listUserCompanyAccess, | |
| setUserCompanyAccess, | |
| setPrincipalGrants, | |
| }; | |
| } | |