import { z } from 'zod' import { organizationIdSchema } from '@/lib/api/contracts/primitives' import { shareAuthTypeSchema } from '@/lib/api/contracts/public-shares' import { defineRouteContract } from '@/lib/api/contracts/types' import { permissionGroupConfigSchema } from '@/lib/permission-groups/types' export const permissionGroupFullConfigSchema = z.object({ allowedIntegrations: z.array(z.string()).nullable(), allowedModelProviders: z.array(z.string()).nullable(), deniedModels: z.array(z.string()).default([]), deniedTools: z.array(z.string()).default([]), hideTraceSpans: z.boolean(), hideKnowledgeBaseTab: z.boolean(), hideTablesTab: z.boolean(), hideCopilot: z.boolean(), hideIntegrationsTab: z.boolean(), hideSecretsTab: z.boolean(), hideApiKeysTab: z.boolean(), hideInboxTab: z.boolean(), hideFilesTab: z.boolean(), disableMcpTools: z.boolean(), disableCustomTools: z.boolean(), disableSkills: z.boolean(), disableInvitations: z.boolean(), disablePublicApi: z.boolean(), disablePublicFileSharing: z.boolean(), allowedFileShareAuthTypes: z.array(shareAuthTypeSchema).nullable(), hideDeployApi: z.boolean(), hideDeployMcp: z.boolean(), hideDeployChatbot: z.boolean(), hideDeployTemplate: z.boolean(), }) export const addPermissionGroupMemberBodySchema = z.object({ userId: z.string().min(1), }) /** Route params for organization-scoped permission-group collection routes (`id` = organizationId). */ export const permissionGroupParamsSchema = z.object({ id: organizationIdSchema, }) /** Route params for a single permission group (`id` = organizationId, `groupId` = permission group id). */ export const permissionGroupDetailParamsSchema = z.object({ id: organizationIdSchema, groupId: z.string().min(1), }) /** A workspace a permission group targets (id + display name). */ export const permissionGroupWorkspaceRefSchema = z.object({ id: z.string(), name: z.string(), }) export type PermissionGroupWorkspaceRef = z.output export const permissionGroupSchema = z.object({ id: z.string(), name: z.string(), description: z.string().nullable(), config: permissionGroupFullConfigSchema, createdBy: z.string(), createdAt: z.string(), updatedAt: z.string(), creatorName: z.string().nullable(), creatorEmail: z.string().nullable(), memberCount: z.number(), isDefault: z.boolean(), /** * Workspaces this group targets. Empty for the default group (which governs * every workspace) and for a non-default group scoped to nothing. */ workspaces: z.array(permissionGroupWorkspaceRefSchema), }) export type PermissionGroup = z.output export const permissionGroupWriteSchema = z.object({ id: z.string(), organizationId: z.string(), name: z.string(), description: z.string().nullable(), config: permissionGroupFullConfigSchema, createdBy: z.string(), createdAt: z.string(), updatedAt: z.string(), isDefault: z.boolean(), /** Ids of targeted workspaces (empty for the default group). */ workspaceIds: z.array(z.string()), }) export type PermissionGroupWrite = z.output export const permissionGroupMemberSchema = z.object({ id: z.string(), userId: z.string(), assignedAt: z.string(), userName: z.string().nullable(), userEmail: z.string().nullable(), userImage: z.string().nullable(), }) export type PermissionGroupMember = z.output export const userPermissionConfigQuerySchema = z.object({ workspaceId: z.string().min(1), }) export const userPermissionConfigSchema = z.object({ permissionGroupId: z.string().nullable(), groupName: z.string().nullable(), config: permissionGroupFullConfigSchema.nullable(), entitled: z.boolean(), /** The workspace's owning organization id (null when the workspace has no org). */ organizationId: z.string().nullable(), /** Whether the caller is an owner/admin of the workspace's owning organization. */ isOrgAdmin: z.boolean(), }) export type UserPermissionConfig = z.output /** Upper bound on how many workspaces a single group can explicitly target. */ export const MAX_PERMISSION_GROUP_WORKSPACES = 500 const workspaceIdsSchema = z.array(z.string().min(1)).max(MAX_PERMISSION_GROUP_WORKSPACES) /** * The one cross-field scope rule shared by create and update: the organization * default group governs every workspace, so it cannot also name specific * workspaces (they would be silently dropped server-side). "Org-wide" is * definitionally `isDefault` — there is no separate flag — so a default group * with no `workspaceIds` is already the all-workspaces case and needs no * assertion here. * * Everything else is left to the routes: a non-default group targets the * workspaces in `workspaceIds` (empty is allowed on update — the group then * governs nothing, since the resolver inner-joins the workspace link table), and * the create route requires at least one workspace up front. */ function refineWorkspaceScope( body: { workspaceIds?: string[]; isDefault?: boolean }, ctx: z.RefinementCtx ) { if (body.isDefault === true && body.workspaceIds && body.workspaceIds.length > 0) { ctx.addIssue({ code: 'custom', path: ['workspaceIds'], message: 'The default group governs all workspaces and cannot target specific workspaces', }) } } export const createPermissionGroupBodySchema = z .object({ name: z.string().trim().min(1).max(100), description: z.string().max(500).optional(), config: permissionGroupConfigSchema.optional(), isDefault: z.boolean().optional(), workspaceIds: workspaceIdsSchema.optional(), }) .superRefine(refineWorkspaceScope) export const updatePermissionGroupBodySchema = z .object({ name: z.string().trim().min(1).max(100).optional(), description: z.string().max(500).nullable().optional(), config: permissionGroupConfigSchema.optional(), isDefault: z.boolean().optional(), workspaceIds: workspaceIdsSchema.optional(), }) .superRefine(refineWorkspaceScope) export const removePermissionGroupMemberQuerySchema = z.object({ memberId: z.string().min(1), }) export const bulkAddPermissionGroupMembersBodySchema = z.object({ userIds: z.array(z.string()).optional(), addAllOrganizationMembers: z.boolean().optional(), }) const successResponseSchema = z.object({ success: z.literal(true), }) export const listPermissionGroupsContract = defineRouteContract({ method: 'GET', path: '/api/organizations/[id]/permission-groups', params: permissionGroupParamsSchema, response: { mode: 'json', schema: z.object({ permissionGroups: z.array(permissionGroupSchema).optional(), }), }, }) export const createPermissionGroupContract = defineRouteContract({ method: 'POST', path: '/api/organizations/[id]/permission-groups', params: permissionGroupParamsSchema, body: createPermissionGroupBodySchema, response: { mode: 'json', schema: z.object({ permissionGroup: permissionGroupWriteSchema, }), }, }) export const getUserPermissionConfigContract = defineRouteContract({ method: 'GET', path: '/api/permission-groups/user', query: userPermissionConfigQuerySchema, response: { mode: 'json', schema: userPermissionConfigSchema, }, }) export const updatePermissionGroupContract = defineRouteContract({ method: 'PUT', path: '/api/organizations/[id]/permission-groups/[groupId]', params: permissionGroupDetailParamsSchema, body: updatePermissionGroupBodySchema, response: { mode: 'json', schema: z.object({ permissionGroup: permissionGroupWriteSchema, }), }, }) export const deletePermissionGroupContract = defineRouteContract({ method: 'DELETE', path: '/api/organizations/[id]/permission-groups/[groupId]', params: permissionGroupDetailParamsSchema, response: { mode: 'json', schema: successResponseSchema, }, }) export const listPermissionGroupMembersContract = defineRouteContract({ method: 'GET', path: '/api/organizations/[id]/permission-groups/[groupId]/members', params: permissionGroupDetailParamsSchema, response: { mode: 'json', schema: z.object({ members: z.array(permissionGroupMemberSchema).optional(), }), }, }) export const removePermissionGroupMemberContract = defineRouteContract({ method: 'DELETE', path: '/api/organizations/[id]/permission-groups/[groupId]/members', params: permissionGroupDetailParamsSchema, query: removePermissionGroupMemberQuerySchema, response: { mode: 'json', schema: successResponseSchema, }, }) export const addPermissionGroupMemberContract = defineRouteContract({ method: 'POST', path: '/api/organizations/[id]/permission-groups/[groupId]/members', params: permissionGroupDetailParamsSchema, body: addPermissionGroupMemberBodySchema, response: { mode: 'json', schema: z.object({ member: z.object({ id: z.string(), permissionGroupId: z.string(), organizationId: z.string(), userId: z.string(), assignedBy: z.string(), assignedAt: z.string(), }), }), }, }) export const bulkAddPermissionGroupMembersContract = defineRouteContract({ method: 'POST', path: '/api/organizations/[id]/permission-groups/[groupId]/members/bulk', params: permissionGroupDetailParamsSchema, body: bulkAddPermissionGroupMembersBodySchema, response: { mode: 'json', schema: z.object({ added: z.number(), // Users not added because they were already in this group. A conflicting // selection fails the whole request (409) rather than being skipped, so // the add is all-or-nothing for conflicts. skipped: z.number(), }), }, }) /** * List the workspaces belonging to an organization, used to populate the * workspace multi-select when scoping a permission group to specific workspaces. */ export const listOrganizationWorkspacesContract = defineRouteContract({ method: 'GET', path: '/api/organizations/[id]/workspaces', params: permissionGroupParamsSchema, response: { mode: 'json', schema: z.object({ workspaces: z.array(permissionGroupWorkspaceRefSchema), }), }, })