| 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 listActiveUserMemberships(companyId: string) { |
| return db |
| .select() |
| .from(companyMemberships) |
| .where( |
| and( |
| eq(companyMemberships.companyId, companyId), |
| eq(companyMemberships.principalType, "user"), |
| eq(companyMemberships.status, "active"), |
| ), |
| ) |
| .orderBy(sql`${companyMemberships.createdAt} asc`); |
| } |
|
|
| 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(), |
| })), |
| ); |
| }); |
| } |
|
|
| async function copyActiveUserMemberships(sourceCompanyId: string, targetCompanyId: string) { |
| const sourceMemberships = await listActiveUserMemberships(sourceCompanyId); |
| for (const membership of sourceMemberships) { |
| await ensureMembership( |
| targetCompanyId, |
| "user", |
| membership.principalId, |
| membership.membershipRole, |
| "active", |
| ); |
| } |
| return sourceMemberships; |
| } |
|
|
| async function listPrincipalGrants( |
| companyId: string, |
| principalType: PrincipalType, |
| principalId: string, |
| ) { |
| return db |
| .select() |
| .from(principalPermissionGrants) |
| .where( |
| and( |
| eq(principalPermissionGrants.companyId, companyId), |
| eq(principalPermissionGrants.principalType, principalType), |
| eq(principalPermissionGrants.principalId, principalId), |
| ), |
| ) |
| .orderBy(principalPermissionGrants.permissionKey); |
| } |
|
|
| async function setPrincipalPermission( |
| companyId: string, |
| principalType: PrincipalType, |
| principalId: string, |
| permissionKey: PermissionKey, |
| enabled: boolean, |
| grantedByUserId: string | null, |
| scope: Record<string, unknown> | null = null, |
| ) { |
| if (!enabled) { |
| await db |
| .delete(principalPermissionGrants) |
| .where( |
| and( |
| eq(principalPermissionGrants.companyId, companyId), |
| eq(principalPermissionGrants.principalType, principalType), |
| eq(principalPermissionGrants.principalId, principalId), |
| eq(principalPermissionGrants.permissionKey, permissionKey), |
| ), |
| ); |
| return; |
| } |
|
|
| await ensureMembership(companyId, principalType, principalId, "member", "active"); |
|
|
| const existing = await db |
| .select() |
| .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); |
|
|
| if (existing) { |
| await db |
| .update(principalPermissionGrants) |
| .set({ |
| scope, |
| grantedByUserId, |
| updatedAt: new Date(), |
| }) |
| .where(eq(principalPermissionGrants.id, existing.id)); |
| return; |
| } |
|
|
| await db.insert(principalPermissionGrants).values({ |
| companyId, |
| principalType, |
| principalId, |
| permissionKey, |
| scope, |
| grantedByUserId, |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }); |
| } |
|
|
| return { |
| isInstanceAdmin, |
| canUser, |
| hasPermission, |
| getMembership, |
| ensureMembership, |
| listMembers, |
| listActiveUserMemberships, |
| copyActiveUserMemberships, |
| setMemberPermissions, |
| promoteInstanceAdmin, |
| demoteInstanceAdmin, |
| listUserCompanyAccess, |
| setUserCompanyAccess, |
| setPrincipalGrants, |
| listPrincipalGrants, |
| setPrincipalPermission, |
| }; |
| } |
|
|