/** * Admin Access Control (Permission Groups) API * * GET /api/v1/admin/access-control * List all permission groups with optional filtering. * * Query Parameters: * - organizationId?: string - Filter by organization ID * * Response: { data: AdminPermissionGroup[], pagination: PaginationMeta } * * DELETE /api/v1/admin/access-control * Delete permission groups scoped to an organization. * Used when an enterprise plan churns to clean up access control data. * * Query Parameters: * - organizationId: string - Delete all permission groups for this organization * - reason?: string - Reason recorded in audit log (default: "Enterprise plan churn cleanup") * * Response: { success: true, deletedCount: number, membersRemoved: number } */ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { organization, permissionGroup, permissionGroupMember, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { count, eq, inArray, sql } from 'drizzle-orm' import { type AdminV1PermissionGroup, adminV1DeleteAccessControlContract, adminV1ListAccessControlContract, } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { withAdminAuth } from '@/app/api/v1/admin/middleware' import { adminValidationErrorResponse, badRequestResponse, internalErrorResponse, singleResponse, } from '@/app/api/v1/admin/responses' const logger = createLogger('AdminAccessControlAPI') export const GET = withRouteHandler( withAdminAuth(async (request) => { const parsed = await parseRequest( adminV1ListAccessControlContract, request, {}, { validationErrorResponse: adminValidationErrorResponse } ) if (!parsed.success) return parsed.response const { organizationId } = parsed.data.query try { const baseQuery = db .select({ id: permissionGroup.id, organizationId: permissionGroup.organizationId, organizationName: organization.name, name: permissionGroup.name, description: permissionGroup.description, isDefault: permissionGroup.isDefault, createdAt: permissionGroup.createdAt, createdByUserId: permissionGroup.createdBy, createdByEmail: user.email, }) .from(permissionGroup) .leftJoin(organization, eq(permissionGroup.organizationId, organization.id)) .leftJoin(user, eq(permissionGroup.createdBy, user.id)) const groups = organizationId ? await baseQuery.where(eq(permissionGroup.organizationId, organizationId)) : await baseQuery const groupsWithCounts = await Promise.all( groups.map(async (group) => { const [memberCount] = await db .select({ count: count() }) .from(permissionGroupMember) .where(eq(permissionGroupMember.permissionGroupId, group.id)) return { id: group.id, organizationId: group.organizationId, organizationName: group.organizationName, name: group.name, description: group.description, isDefault: group.isDefault, memberCount: memberCount?.count ?? 0, createdAt: group.createdAt.toISOString(), createdByUserId: group.createdByUserId, createdByEmail: group.createdByEmail, } as AdminV1PermissionGroup }) ) logger.info('Admin API: Listed permission groups', { organizationId, count: groupsWithCounts.length, }) return singleResponse({ data: groupsWithCounts, pagination: { total: groupsWithCounts.length, limit: groupsWithCounts.length, offset: 0, hasMore: false, }, }) } catch (error) { logger.error('Admin API: Failed to list permission groups', { error, organizationId, }) return internalErrorResponse('Failed to list permission groups') } }) ) export const DELETE = withRouteHandler( withAdminAuth(async (request) => { const parsed = await parseRequest( adminV1DeleteAccessControlContract, request, {}, { validationErrorResponse: adminValidationErrorResponse, } ) if (!parsed.success) return parsed.response const { organizationId, reason: rawReason } = parsed.data.query // The contract's refine guarantees this at the boundary; this explicit guard // narrows the type (avoiding a non-null assertion) and stays correct even if // the contract changes. if (!organizationId) { return badRequestResponse('organizationId is required') } const reason = rawReason || 'Enterprise plan churn cleanup' try { const existingGroups = await db .select({ id: permissionGroup.id, name: permissionGroup.name, }) .from(permissionGroup) .where(eq(permissionGroup.organizationId, organizationId)) if (existingGroups.length === 0) { logger.info('Admin API: No permission groups to delete', { organizationId }) return singleResponse({ success: true, deletedCount: 0, membersRemoved: 0, message: 'No permission groups found for the given scope', }) } const groupIds = existingGroups.map((g) => g.id) const [memberCountResult] = await db .select({ count: sql`count(*)` }) .from(permissionGroupMember) .where(inArray(permissionGroupMember.permissionGroupId, groupIds)) const membersToRemove = Number(memberCountResult?.count ?? 0) await db.delete(permissionGroup).where(inArray(permissionGroup.id, groupIds)) for (const group of existingGroups) { recordAudit({ actorId: 'admin-api', action: AuditAction.PERMISSION_GROUP_DELETED, resourceType: AuditResourceType.PERMISSION_GROUP, resourceId: group.id, resourceName: group.name, description: `Admin API deleted permission group "${group.name}"`, metadata: { reason, organizationId }, request, }) } logger.info('Admin API: Deleted permission groups', { organizationId, deletedCount: existingGroups.length, membersRemoved: membersToRemove, reason, }) return singleResponse({ success: true, deletedCount: existingGroups.length, membersRemoved: membersToRemove, reason, }) } catch (error) { logger.error('Admin API: Failed to delete permission groups', { error, organizationId, }) return internalErrorResponse('Failed to delete permission groups') } }) )