@open-mercato/core 0.6.2-develop.3461.1.605f31c2c9 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/queue-runner.js +15 -3
  3. package/dist/helpers/integration/queue-runner.js.map +2 -2
  4. package/dist/helpers/integration/queue.js +1 -1
  5. package/dist/helpers/integration/queue.js.map +2 -2
  6. package/dist/helpers/integration/salesUi.js +24 -1
  7. package/dist/helpers/integration/salesUi.js.map +2 -2
  8. package/dist/modules/auth/api/roles/acl/route.js +37 -1
  9. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  10. package/dist/modules/auth/api/roles/route.js +36 -2
  11. package/dist/modules/auth/api/roles/route.js.map +2 -2
  12. package/dist/modules/auth/api/users/acl/route.js +36 -1
  13. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  14. package/dist/modules/auth/api/users/route.js +101 -13
  15. package/dist/modules/auth/api/users/route.js.map +2 -2
  16. package/dist/modules/auth/backend/roles/[id]/edit/page.js +32 -14
  17. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  18. package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
  19. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  20. package/dist/modules/auth/backend/users/create/page.js +1 -1
  21. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  22. package/dist/modules/auth/backend/users/roleOptions.js +1 -1
  23. package/dist/modules/auth/backend/users/roleOptions.js.map +2 -2
  24. package/dist/modules/auth/commands/roles.js +8 -0
  25. package/dist/modules/auth/commands/roles.js.map +2 -2
  26. package/dist/modules/auth/commands/users.js +3 -0
  27. package/dist/modules/auth/commands/users.js.map +2 -2
  28. package/dist/modules/auth/components/AclEditor.js +53 -26
  29. package/dist/modules/auth/components/AclEditor.js.map +2 -2
  30. package/dist/modules/auth/frontend/login.js +35 -0
  31. package/dist/modules/auth/frontend/login.js.map +2 -2
  32. package/dist/modules/auth/lib/grantChecks.js +97 -2
  33. package/dist/modules/auth/lib/grantChecks.js.map +2 -2
  34. package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
  35. package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
  36. package/dist/modules/business_rules/lib/rule-evaluator.js +3 -3
  37. package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
  38. package/dist/modules/customers/ai-agents.js +10 -1
  39. package/dist/modules/customers/ai-agents.js.map +2 -2
  40. package/dist/modules/customers/ai-tools/settings-pack.js +55 -1
  41. package/dist/modules/customers/ai-tools/settings-pack.js.map +2 -2
  42. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
  43. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  44. package/dist/modules/customers/lib/personCompanyLinkTable.js +4 -30
  45. package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
  46. package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
  47. package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
  48. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
  49. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  50. package/dist/modules/data_sync/api/options.js +2 -0
  51. package/dist/modules/data_sync/api/options.js.map +2 -2
  52. package/dist/modules/data_sync/api/run.js +77 -58
  53. package/dist/modules/data_sync/api/run.js.map +2 -2
  54. package/dist/modules/data_sync/backend/data-sync/page.js +3 -2
  55. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  56. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +8 -3
  57. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  58. package/dist/modules/integrations/backend/integrations/[id]/page.js +15 -13
  59. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  60. package/dist/modules/integrations/data/validators.js +9 -6
  61. package/dist/modules/integrations/data/validators.js.map +2 -2
  62. package/dist/modules/messages/api/route.js +2 -2
  63. package/dist/modules/messages/api/route.js.map +2 -2
  64. package/dist/modules/messages/commands/messages.js +42 -2
  65. package/dist/modules/messages/commands/messages.js.map +2 -2
  66. package/dist/modules/messages/components/MessagesInboxPageClient.js +70 -89
  67. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  68. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +8 -6
  69. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
  70. package/dist/modules/messages/components/inboxFilters.js +133 -0
  71. package/dist/modules/messages/components/inboxFilters.js.map +7 -0
  72. package/dist/modules/messages/components/messageListLabels.js +14 -0
  73. package/dist/modules/messages/components/messageListLabels.js.map +7 -0
  74. package/dist/modules/messages/components/useMessagesInboxBulkActions.js +47 -23
  75. package/dist/modules/messages/components/useMessagesInboxBulkActions.js.map +2 -2
  76. package/dist/modules/messages/data/validators.js +8 -6
  77. package/dist/modules/messages/data/validators.js.map +2 -2
  78. package/dist/modules/progress/api/jobs/route.js +26 -16
  79. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  80. package/dist/modules/query_index/di.js +8 -6
  81. package/dist/modules/query_index/di.js.map +2 -2
  82. package/dist/modules/query_index/lib/indexer.js +12 -10
  83. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  84. package/dist/modules/query_index/subscribers/upsert_one.js +7 -1
  85. package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
  86. package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
  87. package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
  88. package/dist/modules/sales/commands/documents.js +104 -0
  89. package/dist/modules/sales/commands/documents.js.map +2 -2
  90. package/dist/modules/sales/data/validators.js +20 -1
  91. package/dist/modules/sales/data/validators.js.map +2 -2
  92. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  93. package/dist/modules/sync_excel/api/import/route.js +126 -116
  94. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  95. package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
  96. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  97. package/dist/modules/translations/components/TranslationManager.js +9 -4
  98. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  99. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  100. package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
  101. package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
  102. package/jest.config.cjs +4 -1
  103. package/package.json +15 -16
  104. package/src/helpers/integration/queue-runner.ts +21 -2
  105. package/src/helpers/integration/queue.ts +1 -1
  106. package/src/helpers/integration/salesUi.ts +26 -1
  107. package/src/modules/auth/api/roles/acl/route.ts +39 -1
  108. package/src/modules/auth/api/roles/route.ts +38 -2
  109. package/src/modules/auth/api/users/acl/route.ts +41 -1
  110. package/src/modules/auth/api/users/route.ts +105 -12
  111. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
  112. package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
  113. package/src/modules/auth/backend/users/create/page.tsx +1 -1
  114. package/src/modules/auth/backend/users/roleOptions.ts +2 -1
  115. package/src/modules/auth/commands/roles.ts +8 -0
  116. package/src/modules/auth/commands/users.ts +3 -0
  117. package/src/modules/auth/components/AclEditor.tsx +50 -24
  118. package/src/modules/auth/frontend/login.tsx +40 -0
  119. package/src/modules/auth/lib/grantChecks.ts +116 -2
  120. package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
  121. package/src/modules/business_rules/lib/rule-evaluator.ts +3 -3
  122. package/src/modules/customers/ai-agents.ts +10 -1
  123. package/src/modules/customers/ai-tools/settings-pack.ts +62 -1
  124. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
  125. package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -41
  126. package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
  127. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
  128. package/src/modules/data_sync/api/options.ts +2 -0
  129. package/src/modules/data_sync/api/run.ts +80 -61
  130. package/src/modules/data_sync/backend/data-sync/page.tsx +10 -0
  131. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +17 -3
  132. package/src/modules/data_sync/i18n/de.json +3 -0
  133. package/src/modules/data_sync/i18n/en.json +3 -0
  134. package/src/modules/data_sync/i18n/es.json +3 -0
  135. package/src/modules/data_sync/i18n/pl.json +3 -0
  136. package/src/modules/data_sync/lib/adapter.ts +9 -0
  137. package/src/modules/integrations/backend/integrations/[id]/page.tsx +15 -13
  138. package/src/modules/integrations/data/validators.ts +5 -4
  139. package/src/modules/messages/api/route.ts +2 -2
  140. package/src/modules/messages/commands/messages.ts +54 -2
  141. package/src/modules/messages/components/MessagesInboxPageClient.tsx +80 -92
  142. package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +9 -5
  143. package/src/modules/messages/components/inboxFilters.ts +162 -0
  144. package/src/modules/messages/components/messageListLabels.ts +29 -0
  145. package/src/modules/messages/components/useMessagesInboxBulkActions.ts +58 -26
  146. package/src/modules/messages/data/validators.ts +7 -5
  147. package/src/modules/messages/i18n/de.json +7 -4
  148. package/src/modules/messages/i18n/en.json +7 -4
  149. package/src/modules/messages/i18n/es.json +7 -4
  150. package/src/modules/messages/i18n/pl.json +7 -4
  151. package/src/modules/progress/api/jobs/route.ts +30 -20
  152. package/src/modules/query_index/di.ts +9 -7
  153. package/src/modules/query_index/lib/indexer.ts +13 -11
  154. package/src/modules/query_index/subscribers/upsert_one.ts +9 -1
  155. package/src/modules/sales/api/order-adjustments/route.ts +2 -1
  156. package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
  157. package/src/modules/sales/commands/documents.ts +116 -0
  158. package/src/modules/sales/data/validators.ts +38 -0
  159. package/src/modules/sales/i18n/de.json +2 -0
  160. package/src/modules/sales/i18n/en.json +2 -0
  161. package/src/modules/sales/i18n/es.json +2 -0
  162. package/src/modules/sales/i18n/pl.json +2 -0
  163. package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
  164. package/src/modules/staff/i18n/de.json +1 -0
  165. package/src/modules/staff/i18n/en.json +1 -0
  166. package/src/modules/staff/i18n/es.json +1 -0
  167. package/src/modules/staff/i18n/pl.json +1 -0
  168. package/src/modules/sync_excel/api/import/route.ts +141 -131
  169. package/src/modules/sync_excel/lib/adapters/customers.ts +1 -0
  170. package/src/modules/translations/components/TranslationManager.tsx +9 -4
  171. package/src/modules/workflows/lib/activity-executor.ts +4 -3
  172. package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
  173. package/tsconfig.json +1 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/roles/acl/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { RoleAcl, Role } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { assertActorCanGrantAcl, normalizeGrantFeatureList } from '@open-mercato/core/modules/auth/lib/grantChecks'\n\ntype TaggableCache = { deleteByTags?: (tags: string[]) => Promise<void> | void }\n\nconst getSchema = z.object({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n})\nconst putSchema = z.object({\n roleId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\nconst roleAclResponseSchema = z.object({\n isSuperAdmin: z.boolean(),\n features: z.array(z.string()),\n organizations: z.array(z.string()).nullable(),\n})\n\nconst roleAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst roleAclErrorSchema = z.object({ error: z.string() })\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsed = getSchema.safeParse({\n roleId: url.searchParams.get('roleId'),\n tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })\n const em = container.resolve('em') as EntityManager\n const authTenantId = auth.tenantId ?? null\n const roleFilter: Record<string, unknown> = { id: parsed.data.roleId }\n if (!isSuperAdmin && authTenantId) {\n roleFilter.$or = [{ tenantId: authTenantId }, { tenantId: null }]\n }\n const role = await em.findOne(Role, roleFilter)\n if (!role) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n const roleTenantId = role?.tenantId ? String(role.tenantId) : null\n\n let tenantScope = parsed.data.tenantId ?? roleTenantId ?? authTenantId ?? null\n if (parsed.data.tenantId && parsed.data.tenantId !== tenantScope) {\n if (isSuperAdmin || parsed.data.tenantId === authTenantId) tenantScope = parsed.data.tenantId\n else return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n if (!tenantScope && !isSuperAdmin) tenantScope = authTenantId ?? null\n\n const acl = tenantScope\n ? await em.findOne(RoleAcl, { role, tenantId: tenantScope })\n : null\n const response = acl\n ? {\n isSuperAdmin: !!acl.isSuperAdmin,\n features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],\n organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,\n }\n : { isSuperAdmin: false, features: [], organizations: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.roleId, ...response }],\n idField: 'id',\n resourceKind: 'auth.role_acl',\n organizationId: auth.orgId ?? null,\n tenantId: tenantScope,\n query: { roleId: parsed.data.roleId, tenantId: tenantScope },\n accessType: 'read:item',\n })\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const body = await req.json().catch(() => ({}))\n const parsed = putSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })\n const rbacService = container.resolve('rbacService') as RbacService\n const authTenantId = auth.tenantId ?? null\n const putRoleFilter: Record<string, unknown> = { id: parsed.data.roleId }\n if (!isSuperAdmin && authTenantId) {\n putRoleFilter.$or = [{ tenantId: authTenantId }, { tenantId: null }]\n }\n const role = await em.findOne(Role, putRoleFilter)\n if (!role) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n\n const roleTenantId = role?.tenantId ? String(role.tenantId) : null\n\n let targetTenantId = parsed.data.tenantId ?? roleTenantId ?? authTenantId ?? null\n if (parsed.data.tenantId && parsed.data.tenantId !== targetTenantId) {\n if (isSuperAdmin || parsed.data.tenantId === authTenantId) {\n targetTenantId = parsed.data.tenantId\n } else {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n if (!targetTenantId && !isSuperAdmin) targetTenantId = authTenantId ?? null\n if (!targetTenantId) return NextResponse.json({ error: 'Tenant required' }, { status: 400 })\n\n if (!isSuperAdmin && targetTenantId !== authTenantId) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n let acl = await em.findOne(RoleAcl, { role, tenantId: targetTenantId })\n if (!acl) {\n acl = em.create(RoleAcl, {\n role,\n tenantId: targetTenantId,\n createdAt: new Date(),\n isSuperAdmin: false,\n })\n }\n\n const existingIsSuperAdmin = !!acl.isSuperAdmin\n const existingFeatures = normalizeGrantFeatureList(acl.featuresJson)\n const existingOrganizations = normalizeOrganizations(acl.organizationsJson)\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? existingIsSuperAdmin\n const requestedFeatures = parsed.data.features === undefined\n ? existingFeatures\n : normalizeGrantFeatureList(parsed.data.features)\n const requestedOrganizations = parsed.data.organizations === undefined\n ? existingOrganizations\n : normalizeOrganizations(parsed.data.organizations)\n\n try {\n await assertActorCanGrantAcl({\n em,\n rbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: requestedIsSuperAdmin,\n features: requestedFeatures,\n organizations: requestedOrganizations,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n acl.organizationsJson = requestedOrganizations\n acl.isSuperAdmin = requestedIsSuperAdmin\n acl.featuresJson = requestedFeatures\n await em.persist(acl).flush()\n \n // Invalidate cache for all users in this tenant since role ACL changed\n if (targetTenantId) {\n await rbacService.invalidateTenantCache(targetTenantId)\n // Sidebar nav caches depend on RBAC; invalidate tenant scope nav caches\n try {\n const cache = container.resolve('cache') as TaggableCache | undefined\n if (cache?.deleteByTags) await cache.deleteByTags([`rbac:tenant:${targetTenantId}`])\n } catch {}\n }\n \n return NextResponse.json({\n ok: true,\n sanitized: false,\n })\n}\n\nfunction normalizeOrganizations(organizations: unknown): string[] | null {\n if (!Array.isArray(organizations)) return null\n return normalizeGrantFeatureList(organizations)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Role ACL management',\n methods: {\n GET: {\n summary: 'Fetch role ACL',\n description: 'Returns the feature and organization assignments associated with a role within the current tenant.',\n query: getSchema,\n responses: [\n { status: 200, description: 'Role ACL entry', schema: roleAclResponseSchema },\n { status: 400, description: 'Invalid role id', schema: roleAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: roleAclErrorSchema },\n { status: 404, description: 'Role not found', schema: roleAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update role ACL',\n description: 'Replaces the feature list, super admin flag, and optional organization assignments for a role.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'Role ACL updated', schema: roleAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: roleAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: roleAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: roleAclErrorSchema },\n { status: 404, description: 'Role not found', schema: roleAclErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { RoleAcl, Role } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminRoleTarget,\n normalizeGrantFeatureList,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\n\ntype TaggableCache = { deleteByTags?: (tags: string[]) => Promise<void> | void }\n\nconst getSchema = z.object({\n roleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n})\nconst putSchema = z.object({\n roleId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\nconst roleAclResponseSchema = z.object({\n isSuperAdmin: z.boolean(),\n features: z.array(z.string()),\n organizations: z.array(z.string()).nullable(),\n})\n\nconst roleAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst roleAclErrorSchema = z.object({ error: z.string() })\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsed = getSchema.safeParse({\n roleId: url.searchParams.get('roleId'),\n tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })\n const em = container.resolve('em') as EntityManager\n const authTenantId = auth.tenantId ?? null\n const roleFilter: Record<string, unknown> = { id: parsed.data.roleId }\n if (!isSuperAdmin && authTenantId) {\n roleFilter.$or = [{ tenantId: authTenantId }, { tenantId: null }]\n }\n const role = await em.findOne(Role, roleFilter)\n if (!role) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n const roleTenantId = role?.tenantId ? String(role.tenantId) : null\n\n let tenantScope = parsed.data.tenantId ?? roleTenantId ?? authTenantId ?? null\n if (parsed.data.tenantId && parsed.data.tenantId !== tenantScope) {\n if (isSuperAdmin || parsed.data.tenantId === authTenantId) tenantScope = parsed.data.tenantId\n else return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n if (!tenantScope && !isSuperAdmin) tenantScope = authTenantId ?? null\n\n if (!isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: tenantScope,\n organizationId: auth.orgId ?? null,\n targetRoleId: parsed.data.roleId,\n actorIsSuperAdmin: false,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const acl = tenantScope\n ? await em.findOne(RoleAcl, { role, tenantId: tenantScope })\n : null\n const response = acl\n ? {\n isSuperAdmin: !!acl.isSuperAdmin,\n features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],\n organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,\n }\n : { isSuperAdmin: false, features: [], organizations: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.roleId, ...response }],\n idField: 'id',\n resourceKind: 'auth.role_acl',\n organizationId: auth.orgId ?? null,\n tenantId: tenantScope,\n query: { roleId: parsed.data.roleId, tenantId: tenantScope },\n accessType: 'read:item',\n })\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const body = await req.json().catch(() => ({}))\n const parsed = putSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })\n const rbacService = container.resolve('rbacService') as RbacService\n const authTenantId = auth.tenantId ?? null\n const putRoleFilter: Record<string, unknown> = { id: parsed.data.roleId }\n if (!isSuperAdmin && authTenantId) {\n putRoleFilter.$or = [{ tenantId: authTenantId }, { tenantId: null }]\n }\n const role = await em.findOne(Role, putRoleFilter)\n if (!role) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n\n const roleTenantId = role?.tenantId ? String(role.tenantId) : null\n\n let targetTenantId = parsed.data.tenantId ?? roleTenantId ?? authTenantId ?? null\n if (parsed.data.tenantId && parsed.data.tenantId !== targetTenantId) {\n if (isSuperAdmin || parsed.data.tenantId === authTenantId) {\n targetTenantId = parsed.data.tenantId\n } else {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n if (!targetTenantId && !isSuperAdmin) targetTenantId = authTenantId ?? null\n if (!targetTenantId) return NextResponse.json({ error: 'Tenant required' }, { status: 400 })\n\n if (!isSuperAdmin && targetTenantId !== authTenantId) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n if (!isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n targetRoleId: parsed.data.roleId,\n actorIsSuperAdmin: false,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n let acl = await em.findOne(RoleAcl, { role, tenantId: targetTenantId })\n if (!acl) {\n acl = em.create(RoleAcl, {\n role,\n tenantId: targetTenantId,\n createdAt: new Date(),\n isSuperAdmin: false,\n })\n }\n\n const existingIsSuperAdmin = !!acl.isSuperAdmin\n const existingFeatures = normalizeGrantFeatureList(acl.featuresJson)\n const existingOrganizations = normalizeOrganizations(acl.organizationsJson)\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? existingIsSuperAdmin\n const requestedFeatures = parsed.data.features === undefined\n ? existingFeatures\n : normalizeGrantFeatureList(parsed.data.features)\n const requestedOrganizations = parsed.data.organizations === undefined\n ? existingOrganizations\n : normalizeOrganizations(parsed.data.organizations)\n\n try {\n await assertActorCanGrantAcl({\n em,\n rbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: requestedIsSuperAdmin,\n features: requestedFeatures,\n organizations: requestedOrganizations,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n acl.organizationsJson = requestedOrganizations\n acl.isSuperAdmin = requestedIsSuperAdmin\n acl.featuresJson = requestedFeatures\n await em.persist(acl).flush()\n \n // Invalidate cache for all users in this tenant since role ACL changed\n if (targetTenantId) {\n await rbacService.invalidateTenantCache(targetTenantId)\n // Sidebar nav caches depend on RBAC; invalidate tenant scope nav caches\n try {\n const cache = container.resolve('cache') as TaggableCache | undefined\n if (cache?.deleteByTags) await cache.deleteByTags([`rbac:tenant:${targetTenantId}`])\n } catch {}\n }\n \n return NextResponse.json({\n ok: true,\n sanitized: false,\n })\n}\n\nfunction normalizeOrganizations(organizations: unknown): string[] | null {\n if (!Array.isArray(organizations)) return null\n return normalizeGrantFeatureList(organizations)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Role ACL management',\n methods: {\n GET: {\n summary: 'Fetch role ACL',\n description: 'Returns the feature and organization assignments associated with a role within the current tenant.',\n query: getSchema,\n responses: [\n { status: 200, description: 'Role ACL entry', schema: roleAclResponseSchema },\n { status: 400, description: 'Invalid role id', schema: roleAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: roleAclErrorSchema },\n { status: 404, description: 'Role not found', schema: roleAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update role ACL',\n description: 'Replaces the feature list, super admin flag, and optional organization assignments for a role.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'Role ACL updated', schema: roleAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: roleAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: roleAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: roleAclErrorSchema },\n { status: 404, description: 'Role not found', schema: roleAclErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { logCrudAccess, makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
4
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
5
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
6
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
7
  import { Role, RoleAcl, UserRole } from "@open-mercato/core/modules/auth/data/entities";
@@ -10,6 +11,7 @@ import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fiel
10
11
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
11
12
  import { roleCrudEvents, roleCrudIndexer } from "@open-mercato/core/modules/auth/commands/roles";
12
13
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
14
+ import { assertActorCanModifySuperAdminRoleTarget } from "@open-mercato/core/modules/auth/lib/grantChecks";
13
15
  const querySchema = z.object({
14
16
  id: z.string().uuid().optional(),
15
17
  page: z.coerce.number().min(1).default(1),
@@ -72,7 +74,12 @@ const crud = makeCrudRoute({
72
74
  update: {
73
75
  commandId: "auth.roles.update",
74
76
  schema: rawBodySchema,
75
- mapInput: ({ parsed }) => parsed,
77
+ mapInput: async ({ parsed, ctx }) => {
78
+ if (ctx.request && typeof parsed.id === "string" && parsed.id.length) {
79
+ await assertCanModifySuperAdminRole(ctx.request, parsed.id);
80
+ }
81
+ return parsed;
82
+ },
76
83
  response: () => ({ ok: true })
77
84
  },
78
85
  delete: {
@@ -213,7 +220,34 @@ async function GET(req) {
213
220
  }
214
221
  const POST = crud.POST;
215
222
  const PUT = crud.PUT;
216
- const DELETE = crud.DELETE;
223
+ const DELETE = async (req) => {
224
+ const targetId = new URL(req.url).searchParams.get("id");
225
+ if (targetId) {
226
+ try {
227
+ await assertCanModifySuperAdminRole(req, targetId);
228
+ } catch (err) {
229
+ if (err instanceof CrudHttpError) {
230
+ return NextResponse.json(err.body, { status: err.status });
231
+ }
232
+ throw err;
233
+ }
234
+ }
235
+ return crud.DELETE(req);
236
+ };
237
+ async function assertCanModifySuperAdminRole(req, targetRoleId) {
238
+ const auth = await getAuthFromRequest(req);
239
+ if (!auth?.sub) throw new CrudHttpError(401, { error: "Unauthorized" });
240
+ const container = await createRequestContainer();
241
+ const em = container.resolve("em");
242
+ await assertActorCanModifySuperAdminRoleTarget({
243
+ em,
244
+ rbacService: container.resolve("rbacService"),
245
+ actorUserId: auth.sub,
246
+ tenantId: auth.tenantId ?? null,
247
+ organizationId: auth.orgId ?? null,
248
+ targetRoleId
249
+ });
250
+ }
217
251
  const openApi = {
218
252
  tag: "Authentication & Accounts",
219
253
  summary: "Role management",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/auth/api/roles/route.ts"],
4
- "sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Role, RoleAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { roleCrudEvents, roleCrudIndexer } from '@open-mercato/core/modules/auth/commands/roles'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\n\nconst querySchema = z.object({\n id: z.string().uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n tenantId: z.string().uuid().optional(),\n}).passthrough()\n\nconst roleCreateSchema = z.object({\n name: z.string().min(2).max(100),\n tenantId: z.string().uuid().optional(),\n})\n\nconst roleUpdateSchema = z.object({\n id: z.string().uuid(),\n name: z.string().min(2).max(100).optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nconst roleListItemSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n usersCount: z.number().int().nonnegative(),\n tenantId: z.string().uuid().nullable(),\n tenantIds: z.array(z.string().uuid()).optional(),\n tenantName: z.string().nullable(),\n})\n\nconst roleListResponseSchema = z.object({\n items: z.array(roleListItemSchema),\n total: z.number().int().nonnegative(),\n totalPages: z.number().int().positive(),\n isSuperAdmin: z.boolean().optional(),\n})\n\nconst okResponseSchema = z.object({ ok: z.literal(true) })\n\nconst errorResponseSchema = z.object({ error: z.string() })\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.roles.list'] },\n POST: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\ntype CrudInput = Record<string, unknown>\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: Role,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n actions: {\n create: {\n commandId: 'auth.roles.create',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: ({ result }) => ({ id: String(result.id) }),\n status: 201,\n },\n update: {\n commandId: 'auth.roles.update',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'auth.roles.delete',\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const url = new URL(req.url)\n const parsed = querySchema.safeParse({\n id: url.searchParams.get('id') || undefined,\n page: url.searchParams.get('page') || undefined,\n pageSize: url.searchParams.get('pageSize') || undefined,\n search: url.searchParams.get('search') || undefined,\n tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n let isSuperAdmin = false\n try {\n if (auth.sub) {\n const rbacService = container.resolve('rbacService') as any\n const acl = await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n isSuperAdmin = !!acl?.isSuperAdmin\n }\n } catch (err) {\n console.error('roles: failed to resolve rbac', err)\n }\n const actorTenantId = auth.tenantId ? String(auth.tenantId) : null\n if (!isSuperAdmin && !actorTenantId) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n let superAdminRoleIds: Set<string> | null = null\n if (!isSuperAdmin && actorTenantId) {\n const superAdminAcls = await findWithDecryption(em, RoleAcl, { tenantId: actorTenantId, isSuperAdmin: true }, {}, { tenantId: actorTenantId, organizationId: null })\n if (superAdminAcls.length) {\n superAdminRoleIds = new Set(\n superAdminAcls\n .map((acl) => {\n const roleRef = acl.role\n const idValue = roleRef?.id\n return idValue ? String(idValue) : null\n })\n .filter((id): id is string => !!id),\n )\n } else {\n superAdminRoleIds = new Set()\n }\n }\n const { id, page, pageSize, search, tenantId: requestedTenantId } = parsed.data\n const tenantFilter = isSuperAdmin && requestedTenantId ? String(requestedTenantId) : null\n const filters: any[] = [{ deletedAt: null }]\n if (id) filters.push({ id })\n if (search) filters.push({ name: { $ilike: `%${escapeLikePattern(search)}%` } })\n if (!isSuperAdmin && actorTenantId) {\n filters.push({ tenantId: actorTenantId })\n filters.push({ name: { $ne: 'superadmin' } })\n if (superAdminRoleIds && superAdminRoleIds.size) {\n filters.push({ id: { $nin: Array.from(superAdminRoleIds) } })\n }\n } else if (tenantFilter) {\n filters.push({ tenantId: tenantFilter })\n }\n const where = filters.length > 1 ? { $and: filters } : filters[0]\n const [rows, count] = await em.findAndCount(Role, where, { limit: pageSize, offset: (page - 1) * pageSize })\n const roleIds = rows.map((r: any) => String(r.id))\n const counts: Record<string, number> = {}\n if (roleIds.length) {\n const userRoleFilter: FilterQuery<UserRole> = { role: { $in: roleIds }, deletedAt: null }\n const links = await findWithDecryption(em, UserRole, userRoleFilter, {}, { tenantId: null, organizationId: null })\n for (const l of links) {\n const rid = String((l as any).role?.id || (l as any).role)\n counts[rid] = (counts[rid] || 0) + 1\n }\n }\n const roleTenantIds = rows\n .map((role: any) => (role.tenantId ? String(role.tenantId) : null))\n .filter((tenantId): tenantId is string => typeof tenantId === 'string' && tenantId.length > 0)\n const uniqueTenantIds = Array.from(new Set(roleTenantIds))\n let tenantMap: Record<string, string> = {}\n if (uniqueTenantIds.length) {\n const tenants = await findWithDecryption(em, Tenant, { id: { $in: uniqueTenantIds as any }, deletedAt: null }, {}, { tenantId: null, organizationId: null })\n tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tid = tenant?.id ? String(tenant.id) : null\n if (!tid) return acc\n const rawName = (tenant as any)?.name\n const name = typeof rawName === 'string' && rawName.length > 0 ? rawName : tid\n acc[tid] = name\n return acc\n }, {})\n }\n const tenantByRole: Record<string, string | null> = {}\n for (const role of rows) {\n const rid = String(role.id)\n tenantByRole[rid] = role.tenantId ? String(role.tenantId) : null\n }\n const tenantFallbacks = Array.from(new Set<string | null>([\n auth.tenantId ?? null,\n tenantFilter ?? null,\n ...Object.values(tenantByRole),\n ]))\n const cfByRole = roleIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.auth.role,\n recordIds: roleIds,\n tenantIdByRecord: tenantByRole,\n tenantFallbacks,\n })\n : {}\n const items = rows.map((r: any) => {\n const idStr = String(r.id)\n const tenantId = tenantByRole[idStr]\n const tenantName = tenantId ? tenantMap[tenantId] ?? tenantId : null\n const exposeTenant = isSuperAdmin || (tenantId && auth.tenantId && tenantId === auth.tenantId)\n return {\n id: idStr,\n name: String(r.name),\n usersCount: counts[idStr] || 0,\n tenantId: tenantId ?? null,\n tenantIds: exposeTenant && tenantId ? [tenantId] : [],\n tenantName: exposeTenant ? tenantName : null,\n ...(cfByRole[idStr] || {}),\n }\n })\n const totalPages = Math.max(1, Math.ceil(count / pageSize))\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'auth.role',\n organizationId: null,\n tenantId: auth.tenantId ?? null,\n query: parsed.data,\n accessType: id ? 'read:item' : undefined,\n })\n return NextResponse.json({ items, total: count, totalPages, isSuperAdmin })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Role management',\n methods: {\n GET: {\n summary: 'List roles',\n description:\n 'Returns available roles within the current tenant. Super administrators receive visibility across tenants.',\n query: querySchema,\n responses: [\n { status: 200, description: 'Role collection', schema: roleListResponseSchema },\n ],\n },\n POST: {\n summary: 'Create role',\n description: 'Creates a new role for the current tenant or globally when `tenantId` is omitted.',\n requestBody: {\n contentType: 'application/json',\n schema: roleCreateSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Role created',\n schema: z.object({ id: z.string().uuid() }),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n ],\n },\n PUT: {\n summary: 'Update role',\n description: 'Updates mutable fields on an existing role.',\n requestBody: {\n contentType: 'application/json',\n schema: roleUpdateSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Role updated',\n schema: okResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'Role not found', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Delete role',\n description: 'Deletes a role by identifier. Fails when users remain assigned.',\n query: z.object({ id: z.string().uuid().describe('Role identifier') }),\n responses: [\n { status: 200, description: 'Role deleted', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Role cannot be deleted', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'Role not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,eAAe,qBAAqB;AAC7C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,MAAM,SAAS,gBAAgB;AACxC,SAAS,cAAc;AACvB,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC,SAAS,0BAA0B;AAEnC,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAElC,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC/B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC,EAAE,YAAY;AAEf,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC/B,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1C,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAED,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO;AAAA,EACf,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACzC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAC/C,YAAY,EAAE,OAAO,EAAE,SAAS;AAClC,CAAC;AAED,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,OAAO,EAAE,MAAM,kBAAkB;AAAA,EACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EACtC,cAAc,EAAE,QAAQ,EAAE,SAAS;AACrC,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAEzD,MAAM,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAE1D,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,iBAAiB,EAAE;AAAA,EAC/D,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AAAA,EAClE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AAAA,EACjE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACtE;AAEO,MAAM,WAAW;AAExB,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAG/C,MAAM,OAAO,cAA6D;AAAA,EACxE,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,CAAC,EAAE,OAAO,MAAM;AAAA,MAC1B,UAAU,CAAC,EAAE,OAAO,OAAO,EAAE,IAAI,OAAO,OAAO,EAAE,EAAE;AAAA,MACnD,QAAQ;AAAA,IACV;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,CAAC,EAAE,OAAO,MAAM;AAAA,MAC1B,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,EACF;AACF,CAAC;AAED,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,YAAY,EAAE,CAAC;AAC1E,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,SAAS,YAAY,UAAU;AAAA,IACnC,IAAI,IAAI,aAAa,IAAI,IAAI,KAAK;AAAA,IAClC,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,IACtC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,IAC9C,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,IAC1C,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,EAChD,CAAC;AACD,MAAI,CAAC,OAAO,QAAS,QAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,YAAY,EAAE,CAAC;AACpF,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI;AAClC,MAAI,eAAe;AACnB,MAAI;AACF,QAAI,KAAK,KAAK;AACZ,YAAM,cAAc,UAAU,QAAQ,aAAa;AACnD,YAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK,EAAE,UAAU,KAAK,YAAY,MAAM,gBAAgB,KAAK,SAAS,KAAK,CAAC;AACvH,qBAAe,CAAC,CAAC,KAAK;AAAA,IACxB;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,MAAM,iCAAiC,GAAG;AAAA,EACpD;AACA,QAAM,gBAAgB,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI;AAC9D,MAAI,CAAC,gBAAgB,CAAC,eAAe;AACnC,WAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,YAAY,GAAG,aAAa,CAAC;AAAA,EAC/E;AACA,MAAI,oBAAwC;AAC5C,MAAI,CAAC,gBAAgB,eAAe;AAClC,UAAM,iBAAiB,MAAM,mBAAmB,IAAI,SAAS,EAAE,UAAU,eAAe,cAAc,KAAK,GAAG,CAAC,GAAG,EAAE,UAAU,eAAe,gBAAgB,KAAK,CAAC;AACnK,QAAI,eAAe,QAAQ;AACzB,0BAAoB,IAAI;AAAA,QACtB,eACG,IAAI,CAAC,QAAQ;AACZ,gBAAM,UAAU,IAAI;AACpB,gBAAM,UAAU,SAAS;AACzB,iBAAO,UAAU,OAAO,OAAO,IAAI;AAAA,QACrC,CAAC,EACA,OAAO,CAACA,QAAqB,CAAC,CAACA,GAAE;AAAA,MACtC;AAAA,IACF,OAAO;AACL,0BAAoB,oBAAI,IAAI;AAAA,IAC9B;AAAA,EACF;AACA,QAAM,EAAE,IAAI,MAAM,UAAU,QAAQ,UAAU,kBAAkB,IAAI,OAAO;AAC3E,QAAM,eAAe,gBAAgB,oBAAoB,OAAO,iBAAiB,IAAI;AACrF,QAAM,UAAiB,CAAC,EAAE,WAAW,KAAK,CAAC;AAC3C,MAAI,GAAI,SAAQ,KAAK,EAAE,GAAG,CAAC;AAC3B,MAAI,OAAQ,SAAQ,KAAK,EAAE,MAAM,EAAE,QAAQ,IAAI,kBAAkB,MAAM,CAAC,IAAI,EAAE,CAAC;AAC/E,MAAI,CAAC,gBAAgB,eAAe;AAClC,YAAQ,KAAK,EAAE,UAAU,cAAc,CAAC;AACxC,YAAQ,KAAK,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,CAAC;AAC5C,QAAI,qBAAqB,kBAAkB,MAAM;AAC/C,cAAQ,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,KAAK,iBAAiB,EAAE,EAAE,CAAC;AAAA,IAC9D;AAAA,EACF,WAAW,cAAc;AACvB,YAAQ,KAAK,EAAE,UAAU,aAAa,CAAC;AAAA,EACzC;AACA,QAAM,QAAQ,QAAQ,SAAS,IAAI,EAAE,MAAM,QAAQ,IAAI,QAAQ,CAAC;AAChE,QAAM,CAAC,MAAM,KAAK,IAAI,MAAM,GAAG,aAAa,MAAM,OAAO,EAAE,OAAO,UAAU,SAAS,OAAO,KAAK,SAAS,CAAC;AAC3G,QAAM,UAAU,KAAK,IAAI,CAAC,MAAW,OAAO,EAAE,EAAE,CAAC;AACjD,QAAM,SAAiC,CAAC;AACxC,MAAI,QAAQ,QAAQ;AAClB,UAAM,iBAAwC,EAAE,MAAM,EAAE,KAAK,QAAQ,GAAG,WAAW,KAAK;AACxF,UAAM,QAAQ,MAAM,mBAAmB,IAAI,UAAU,gBAAgB,CAAC,GAAG,EAAE,UAAU,MAAM,gBAAgB,KAAK,CAAC;AACjH,eAAW,KAAK,OAAO;AACrB,YAAM,MAAM,OAAQ,EAAU,MAAM,MAAO,EAAU,IAAI;AACzD,aAAO,GAAG,KAAK,OAAO,GAAG,KAAK,KAAK;AAAA,IACrC;AAAA,EACF;AACA,QAAM,gBAAgB,KACnB,IAAI,CAAC,SAAe,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI,IAAK,EACjE,OAAO,CAAC,aAAiC,OAAO,aAAa,YAAY,SAAS,SAAS,CAAC;AAC/F,QAAM,kBAAkB,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC;AACzD,MAAI,YAAoC,CAAC;AACzC,MAAI,gBAAgB,QAAQ;AAC1B,UAAM,UAAU,MAAM,mBAAmB,IAAI,QAAQ,EAAE,IAAI,EAAE,KAAK,gBAAuB,GAAG,WAAW,KAAK,GAAG,CAAC,GAAG,EAAE,UAAU,MAAM,gBAAgB,KAAK,CAAC;AAC3J,gBAAY,QAAQ,OAA+B,CAAC,KAAK,WAAW;AAClE,YAAM,MAAM,QAAQ,KAAK,OAAO,OAAO,EAAE,IAAI;AAC7C,UAAI,CAAC,IAAK,QAAO;AACjB,YAAM,UAAW,QAAgB;AACjC,YAAM,OAAO,OAAO,YAAY,YAAY,QAAQ,SAAS,IAAI,UAAU;AAC3E,UAAI,GAAG,IAAI;AACX,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAAA,EACP;AACA,QAAM,eAA8C,CAAC;AACrD,aAAW,QAAQ,MAAM;AACvB,UAAM,MAAM,OAAO,KAAK,EAAE;AAC1B,iBAAa,GAAG,IAAI,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI;AAAA,EAC9D;AACA,QAAM,kBAAkB,MAAM,KAAK,oBAAI,IAAmB;AAAA,IACxD,KAAK,YAAY;AAAA,IACjB,gBAAgB;AAAA,IAChB,GAAG,OAAO,OAAO,YAAY;AAAA,EAC/B,CAAC,CAAC;AACF,QAAM,WAAW,QAAQ,SACrB,MAAM,sBAAsB;AAAA,IAC1B;AAAA,IACA,UAAU,EAAE,KAAK;AAAA,IACjB,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB;AAAA,EACF,CAAC,IACD,CAAC;AACL,QAAM,QAAQ,KAAK,IAAI,CAAC,MAAW;AACjC,UAAM,QAAQ,OAAO,EAAE,EAAE;AACzB,UAAM,WAAW,aAAa,KAAK;AACnC,UAAM,aAAa,WAAW,UAAU,QAAQ,KAAK,WAAW;AAChE,UAAM,eAAe,gBAAiB,YAAY,KAAK,YAAY,aAAa,KAAK;AACrF,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,MAAM,OAAO,EAAE,IAAI;AAAA,MACnB,YAAY,OAAO,KAAK,KAAK;AAAA,MAC7B,UAAU,YAAY;AAAA,MACtB,WAAW,gBAAgB,WAAW,CAAC,QAAQ,IAAI,CAAC;AAAA,MACpD,YAAY,eAAe,aAAa;AAAA,MACxC,GAAI,SAAS,KAAK,KAAK,CAAC;AAAA,IAC1B;AAAA,EACF,CAAC;AACD,QAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,QAAQ,CAAC;AAC1D,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA,SAAS;AAAA,IACT,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,UAAU,KAAK,YAAY;AAAA,IAC3B,OAAO,OAAO;AAAA,IACd,YAAY,KAAK,cAAc;AAAA,EACjC,CAAC;AACD,SAAO,aAAa,KAAK,EAAE,OAAO,OAAO,OAAO,YAAY,aAAa,CAAC;AAC5E;AAEO,MAAM,OAAO,KAAK;AAClB,MAAM,MAAM,KAAK;AACjB,MAAM,SAAS,KAAK;AAEpB,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MACF,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,uBAAuB;AAAA,MAChF;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,QAC5C;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,MAC1E;AAAA,IACF;AAAA,IACA,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,oBAAoB;AAAA,MAC5E;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,MACb,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;AAAA,MACrE,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,iBAAiB;AAAA,MACvE;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,oBAAoB;AAAA,QAClF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,oBAAoB;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Role, RoleAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { roleCrudEvents, roleCrudIndexer } from '@open-mercato/core/modules/auth/commands/roles'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanModifySuperAdminRoleTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nconst querySchema = z.object({\n id: z.string().uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n tenantId: z.string().uuid().optional(),\n}).passthrough()\n\nconst roleCreateSchema = z.object({\n name: z.string().min(2).max(100),\n tenantId: z.string().uuid().optional(),\n})\n\nconst roleUpdateSchema = z.object({\n id: z.string().uuid(),\n name: z.string().min(2).max(100).optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nconst roleListItemSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n usersCount: z.number().int().nonnegative(),\n tenantId: z.string().uuid().nullable(),\n tenantIds: z.array(z.string().uuid()).optional(),\n tenantName: z.string().nullable(),\n})\n\nconst roleListResponseSchema = z.object({\n items: z.array(roleListItemSchema),\n total: z.number().int().nonnegative(),\n totalPages: z.number().int().positive(),\n isSuperAdmin: z.boolean().optional(),\n})\n\nconst okResponseSchema = z.object({ ok: z.literal(true) })\n\nconst errorResponseSchema = z.object({ error: z.string() })\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.roles.list'] },\n POST: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.roles.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\ntype CrudInput = Record<string, unknown>\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: Role,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: roleCrudEvents,\n indexer: roleCrudIndexer,\n actions: {\n create: {\n commandId: 'auth.roles.create',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: ({ result }) => ({ id: String(result.id) }),\n status: 201,\n },\n update: {\n commandId: 'auth.roles.update',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n if (ctx.request && typeof parsed.id === 'string' && parsed.id.length) {\n await assertCanModifySuperAdminRole(ctx.request, parsed.id)\n }\n return parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'auth.roles.delete',\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const url = new URL(req.url)\n const parsed = querySchema.safeParse({\n id: url.searchParams.get('id') || undefined,\n page: url.searchParams.get('page') || undefined,\n pageSize: url.searchParams.get('pageSize') || undefined,\n search: url.searchParams.get('search') || undefined,\n tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n let isSuperAdmin = false\n try {\n if (auth.sub) {\n const rbacService = container.resolve('rbacService') as any\n const acl = await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n isSuperAdmin = !!acl?.isSuperAdmin\n }\n } catch (err) {\n console.error('roles: failed to resolve rbac', err)\n }\n const actorTenantId = auth.tenantId ? String(auth.tenantId) : null\n if (!isSuperAdmin && !actorTenantId) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n let superAdminRoleIds: Set<string> | null = null\n if (!isSuperAdmin && actorTenantId) {\n const superAdminAcls = await findWithDecryption(em, RoleAcl, { tenantId: actorTenantId, isSuperAdmin: true }, {}, { tenantId: actorTenantId, organizationId: null })\n if (superAdminAcls.length) {\n superAdminRoleIds = new Set(\n superAdminAcls\n .map((acl) => {\n const roleRef = acl.role\n const idValue = roleRef?.id\n return idValue ? String(idValue) : null\n })\n .filter((id): id is string => !!id),\n )\n } else {\n superAdminRoleIds = new Set()\n }\n }\n const { id, page, pageSize, search, tenantId: requestedTenantId } = parsed.data\n const tenantFilter = isSuperAdmin && requestedTenantId ? String(requestedTenantId) : null\n const filters: any[] = [{ deletedAt: null }]\n if (id) filters.push({ id })\n if (search) filters.push({ name: { $ilike: `%${escapeLikePattern(search)}%` } })\n if (!isSuperAdmin && actorTenantId) {\n filters.push({ tenantId: actorTenantId })\n filters.push({ name: { $ne: 'superadmin' } })\n if (superAdminRoleIds && superAdminRoleIds.size) {\n filters.push({ id: { $nin: Array.from(superAdminRoleIds) } })\n }\n } else if (tenantFilter) {\n filters.push({ tenantId: tenantFilter })\n }\n const where = filters.length > 1 ? { $and: filters } : filters[0]\n const [rows, count] = await em.findAndCount(Role, where, { limit: pageSize, offset: (page - 1) * pageSize })\n const roleIds = rows.map((r: any) => String(r.id))\n const counts: Record<string, number> = {}\n if (roleIds.length) {\n const userRoleFilter: FilterQuery<UserRole> = { role: { $in: roleIds }, deletedAt: null }\n const links = await findWithDecryption(em, UserRole, userRoleFilter, {}, { tenantId: null, organizationId: null })\n for (const l of links) {\n const rid = String((l as any).role?.id || (l as any).role)\n counts[rid] = (counts[rid] || 0) + 1\n }\n }\n const roleTenantIds = rows\n .map((role: any) => (role.tenantId ? String(role.tenantId) : null))\n .filter((tenantId): tenantId is string => typeof tenantId === 'string' && tenantId.length > 0)\n const uniqueTenantIds = Array.from(new Set(roleTenantIds))\n let tenantMap: Record<string, string> = {}\n if (uniqueTenantIds.length) {\n const tenants = await findWithDecryption(em, Tenant, { id: { $in: uniqueTenantIds as any }, deletedAt: null }, {}, { tenantId: null, organizationId: null })\n tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tid = tenant?.id ? String(tenant.id) : null\n if (!tid) return acc\n const rawName = (tenant as any)?.name\n const name = typeof rawName === 'string' && rawName.length > 0 ? rawName : tid\n acc[tid] = name\n return acc\n }, {})\n }\n const tenantByRole: Record<string, string | null> = {}\n for (const role of rows) {\n const rid = String(role.id)\n tenantByRole[rid] = role.tenantId ? String(role.tenantId) : null\n }\n const tenantFallbacks = Array.from(new Set<string | null>([\n auth.tenantId ?? null,\n tenantFilter ?? null,\n ...Object.values(tenantByRole),\n ]))\n const cfByRole = roleIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.auth.role,\n recordIds: roleIds,\n tenantIdByRecord: tenantByRole,\n tenantFallbacks,\n })\n : {}\n const items = rows.map((r: any) => {\n const idStr = String(r.id)\n const tenantId = tenantByRole[idStr]\n const tenantName = tenantId ? tenantMap[tenantId] ?? tenantId : null\n const exposeTenant = isSuperAdmin || (tenantId && auth.tenantId && tenantId === auth.tenantId)\n return {\n id: idStr,\n name: String(r.name),\n usersCount: counts[idStr] || 0,\n tenantId: tenantId ?? null,\n tenantIds: exposeTenant && tenantId ? [tenantId] : [],\n tenantName: exposeTenant ? tenantName : null,\n ...(cfByRole[idStr] || {}),\n }\n })\n const totalPages = Math.max(1, Math.ceil(count / pageSize))\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'auth.role',\n organizationId: null,\n tenantId: auth.tenantId ?? null,\n query: parsed.data,\n accessType: id ? 'read:item' : undefined,\n })\n return NextResponse.json({ items, total: count, totalPages, isSuperAdmin })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = async (req: Request) => {\n const targetId = new URL(req.url).searchParams.get('id')\n if (targetId) {\n try {\n await assertCanModifySuperAdminRole(req, targetId)\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n }\n return crud.DELETE(req)\n}\n\nasync function assertCanModifySuperAdminRole(req: Request, targetRoleId: string) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n await assertActorCanModifySuperAdminRoleTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetRoleId,\n })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Role management',\n methods: {\n GET: {\n summary: 'List roles',\n description:\n 'Returns available roles within the current tenant. Super administrators receive visibility across tenants.',\n query: querySchema,\n responses: [\n { status: 200, description: 'Role collection', schema: roleListResponseSchema },\n ],\n },\n POST: {\n summary: 'Create role',\n description: 'Creates a new role for the current tenant or globally when `tenantId` is omitted.',\n requestBody: {\n contentType: 'application/json',\n schema: roleCreateSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Role created',\n schema: z.object({ id: z.string().uuid() }),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n ],\n },\n PUT: {\n summary: 'Update role',\n description: 'Updates mutable fields on an existing role.',\n requestBody: {\n contentType: 'application/json',\n schema: roleUpdateSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Role updated',\n schema: okResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'Role not found', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Delete role',\n description: 'Deletes a role by identifier. Fails when users remain assigned.',\n query: z.object({ id: z.string().uuid().describe('Role identifier') }),\n responses: [\n { status: 200, description: 'Role deleted', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Role cannot be deleted', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'Role not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["id"]
7
7
  }
@@ -3,8 +3,9 @@ import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { logCrudAccess } from "@open-mercato/shared/lib/crud/factory";
6
- import { forbidden } from "@open-mercato/shared/lib/crud/errors";
6
+ import { forbidden, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
7
7
  import { UserAcl } from "@open-mercato/core/modules/auth/data/entities";
8
+ import { assertActorCanModifySuperAdminUserTarget } from "@open-mercato/core/modules/auth/lib/grantChecks";
8
9
  const getSchema = z.object({ userId: z.string().uuid() });
9
10
  const putSchema = z.object({
10
11
  userId: z.string().uuid(),
@@ -35,6 +36,24 @@ async function GET(req) {
35
36
  if (!parsed.success) return NextResponse.json({ error: "Invalid input" }, { status: 400 });
36
37
  const container = await createRequestContainer();
37
38
  const em = container.resolve("em");
39
+ const rbacService = container.resolve("rbacService");
40
+ const actorAcl = auth.sub ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null }) : null;
41
+ if (!actorAcl?.isSuperAdmin && auth.sub) {
42
+ try {
43
+ await assertActorCanModifySuperAdminUserTarget({
44
+ em,
45
+ rbacService,
46
+ actorUserId: auth.sub,
47
+ tenantId: auth.tenantId ?? null,
48
+ organizationId: auth.orgId ?? null,
49
+ targetUserId: parsed.data.userId,
50
+ actorIsSuperAdmin: false
51
+ });
52
+ } catch (err) {
53
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
54
+ throw err;
55
+ }
56
+ }
38
57
  const acl = await em.findOne(UserAcl, { user: parsed.data.userId, tenantId: auth.tenantId });
39
58
  const response = acl ? {
40
59
  hasCustomAcl: true,
@@ -67,6 +86,22 @@ async function PUT(req) {
67
86
  const rbacService = container.resolve("rbacService");
68
87
  const actorAcl = auth.sub ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null }) : null;
69
88
  const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin;
89
+ if (!actorIsSuperAdmin && auth.sub) {
90
+ try {
91
+ await assertActorCanModifySuperAdminUserTarget({
92
+ em,
93
+ rbacService,
94
+ actorUserId: auth.sub,
95
+ tenantId: auth.tenantId ?? null,
96
+ organizationId: auth.orgId ?? null,
97
+ targetUserId: parsed.data.userId,
98
+ actorIsSuperAdmin: false
99
+ });
100
+ } catch (err) {
101
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
102
+ throw err;
103
+ }
104
+ }
70
105
  const requestedFeatures = normalizeFeatureList(parsed.data.features);
71
106
  const organizations = Array.isArray(parsed.data.organizations) ? parsed.data.organizations : null;
72
107
  let acl = await em.findOne(UserAcl, { user: parsed.data.userId, tenantId: auth.tenantId });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/users/acl/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport { UserAcl } from '@open-mercato/core/modules/auth/data/entities'\n\nconst getSchema = z.object({ userId: z.string().uuid() })\nconst putSchema = z.object({\n userId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\nconst userAclResponseSchema = z.object({\n hasCustomAcl: z.boolean(),\n isSuperAdmin: z.boolean(),\n features: z.array(z.string()),\n organizations: z.array(z.string()).nullable(),\n})\n\nconst userAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst userAclErrorSchema = z.object({ error: z.string() })\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsed = getSchema.safeParse({ userId: url.searchParams.get('userId') })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n const response = acl\n ? {\n hasCustomAcl: true,\n isSuperAdmin: !!acl.isSuperAdmin,\n features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],\n organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,\n }\n : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.userId, ...response }],\n idField: 'id',\n resourceKind: 'auth.user_acl',\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n query: { userId: parsed.data.userId },\n accessType: 'read:item',\n })\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const body = await req.json().catch(() => ({}))\n const parsed = putSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n const requestedFeatures = normalizeFeatureList(parsed.data.features)\n const organizations = Array.isArray(parsed.data.organizations) ? parsed.data.organizations : null\n\n let acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false\n const existingFeatures = acl && Array.isArray(acl.featuresJson) ? normalizeFeatureList(acl.featuresJson) : []\n\n const effectiveFeatures = actorIsSuperAdmin\n ? requestedFeatures\n : sanitizeTenantFeatures(requestedFeatures)\n\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? false\n let effectiveIsSuperAdmin = requestedIsSuperAdmin\n\n if (!actorIsSuperAdmin) {\n if (requestedIsSuperAdmin && !existingIsSuperAdmin) {\n throw forbidden('Only super administrators can grant super admin access.')\n }\n if (existingIsSuperAdmin && requestedIsSuperAdmin === false) {\n effectiveIsSuperAdmin = false\n } else {\n effectiveIsSuperAdmin = existingIsSuperAdmin\n }\n }\n\n const hasCustomAcl = effectiveIsSuperAdmin || effectiveFeatures.length > 0\n\n if (!hasCustomAcl) {\n if (acl) await em.remove(acl).flush()\n } else {\n if (!acl) {\n acl = em.create(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n }\n const aclRecord = acl as any\n aclRecord.isSuperAdmin = effectiveIsSuperAdmin\n aclRecord.featuresJson = effectiveFeatures\n aclRecord.organizationsJson = organizations\n await em.persist(acl).flush()\n }\n\n // Invalidate cache for this user\n await rbacService.invalidateUserCache(parsed.data.userId)\n try {\n const cache = container.resolve('cache') as any\n if (cache) await cache.deleteByTags([`rbac:user:${parsed.data.userId}`])\n } catch {}\n\n return NextResponse.json({\n ok: true,\n sanitized: !actorIsSuperAdmin && (hasRestrictedChanges(requestedFeatures, effectiveFeatures, existingFeatures) || requestedIsSuperAdmin !== effectiveIsSuperAdmin),\n })\n}\n\nfunction normalizeFeatureList(features: unknown): string[] {\n if (!Array.isArray(features)) return []\n const dedup = new Set<string>()\n for (const value of features) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (!trimmed) continue\n dedup.add(trimmed)\n }\n return Array.from(dedup)\n}\n\nfunction sanitizeTenantFeatures(features: string[]): string[] {\n return features.filter((feature) => !isTenantRestrictedFeature(feature))\n}\n\nfunction isTenantRestrictedFeature(feature: string): boolean {\n if (feature === '*' || feature === 'directory.*') return true\n if (feature.startsWith('directory.tenants')) return true\n return false\n}\n\nfunction hasRestrictedChanges(requested: string[], effective: string[], existing: string[]): boolean {\n if (requested.length === effective.length) return false\n const effectiveSet = new Set(effective)\n const existingSet = new Set(existing)\n // If the effective set matches existing, we only trimmed restricted duplicates and should not report\n if (effectiveSet.size === existingSet.size) {\n let identical = true\n for (const value of effectiveSet) {\n if (!existingSet.has(value)) {\n identical = false\n break\n }\n }\n if (identical) return false\n }\n return true\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'User ACL management',\n methods: {\n GET: {\n summary: 'Fetch user ACL',\n description: 'Returns custom ACL overrides for a user within the current tenant, if any.',\n query: getSchema,\n responses: [\n { status: 200, description: 'User ACL entry', schema: userAclResponseSchema },\n { status: 400, description: 'Invalid user id', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update user ACL',\n description: 'Configures per-user ACL overrides, including super admin access, feature list, and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'User ACL updated', schema: userAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: userAclErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { UserAcl } from '@open-mercato/core/modules/auth/data/entities'\nimport { assertActorCanModifySuperAdminUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nconst getSchema = z.object({ userId: z.string().uuid() })\nconst putSchema = z.object({\n userId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\nconst userAclResponseSchema = z.object({\n hasCustomAcl: z.boolean(),\n isSuperAdmin: z.boolean(),\n features: z.array(z.string()),\n organizations: z.array(z.string()).nullable(),\n})\n\nconst userAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst userAclErrorSchema = z.object({ error: z.string() })\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsed = getSchema.safeParse({ userId: url.searchParams.get('userId') })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n : null\n if (!actorAcl?.isSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n const response = acl\n ? {\n hasCustomAcl: true,\n isSuperAdmin: !!acl.isSuperAdmin,\n features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],\n organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,\n }\n : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.userId, ...response }],\n idField: 'id',\n resourceKind: 'auth.user_acl',\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n query: { userId: parsed.data.userId },\n accessType: 'read:item',\n })\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const body = await req.json().catch(() => ({}))\n const parsed = putSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n if (!actorIsSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const requestedFeatures = normalizeFeatureList(parsed.data.features)\n const organizations = Array.isArray(parsed.data.organizations) ? parsed.data.organizations : null\n\n let acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false\n const existingFeatures = acl && Array.isArray(acl.featuresJson) ? normalizeFeatureList(acl.featuresJson) : []\n\n const effectiveFeatures = actorIsSuperAdmin\n ? requestedFeatures\n : sanitizeTenantFeatures(requestedFeatures)\n\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? false\n let effectiveIsSuperAdmin = requestedIsSuperAdmin\n\n if (!actorIsSuperAdmin) {\n if (requestedIsSuperAdmin && !existingIsSuperAdmin) {\n throw forbidden('Only super administrators can grant super admin access.')\n }\n if (existingIsSuperAdmin && requestedIsSuperAdmin === false) {\n effectiveIsSuperAdmin = false\n } else {\n effectiveIsSuperAdmin = existingIsSuperAdmin\n }\n }\n\n const hasCustomAcl = effectiveIsSuperAdmin || effectiveFeatures.length > 0\n\n if (!hasCustomAcl) {\n if (acl) await em.remove(acl).flush()\n } else {\n if (!acl) {\n acl = em.create(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n }\n const aclRecord = acl as any\n aclRecord.isSuperAdmin = effectiveIsSuperAdmin\n aclRecord.featuresJson = effectiveFeatures\n aclRecord.organizationsJson = organizations\n await em.persist(acl).flush()\n }\n\n // Invalidate cache for this user\n await rbacService.invalidateUserCache(parsed.data.userId)\n try {\n const cache = container.resolve('cache') as any\n if (cache) await cache.deleteByTags([`rbac:user:${parsed.data.userId}`])\n } catch {}\n\n return NextResponse.json({\n ok: true,\n sanitized: !actorIsSuperAdmin && (hasRestrictedChanges(requestedFeatures, effectiveFeatures, existingFeatures) || requestedIsSuperAdmin !== effectiveIsSuperAdmin),\n })\n}\n\nfunction normalizeFeatureList(features: unknown): string[] {\n if (!Array.isArray(features)) return []\n const dedup = new Set<string>()\n for (const value of features) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (!trimmed) continue\n dedup.add(trimmed)\n }\n return Array.from(dedup)\n}\n\nfunction sanitizeTenantFeatures(features: string[]): string[] {\n return features.filter((feature) => !isTenantRestrictedFeature(feature))\n}\n\nfunction isTenantRestrictedFeature(feature: string): boolean {\n if (feature === '*' || feature === 'directory.*') return true\n if (feature.startsWith('directory.tenants')) return true\n return false\n}\n\nfunction hasRestrictedChanges(requested: string[], effective: string[], existing: string[]): boolean {\n if (requested.length === effective.length) return false\n const effectiveSet = new Set(effective)\n const existingSet = new Set(existing)\n // If the effective set matches existing, we only trimmed restricted duplicates and should not report\n if (effectiveSet.size === existingSet.size) {\n let identical = true\n for (const value of effectiveSet) {\n if (!existingSet.has(value)) {\n identical = false\n break\n }\n }\n if (identical) return false\n }\n return true\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'User ACL management',\n methods: {\n GET: {\n summary: 'Fetch user ACL',\n description: 'Returns custom ACL overrides for a user within the current tenant, if any.',\n query: getSchema,\n responses: [\n { status: 200, description: 'User ACL entry', schema: userAclResponseSchema },\n { status: 400, description: 'Invalid user id', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update user ACL',\n description: 'Configures per-user ACL overrides, including super admin access, feature list, and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'User ACL updated', schema: userAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: userAclErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,7 +9,11 @@ import { Organization, Tenant } from "@open-mercato/core/modules/directory/data/
9
9
  import { E } from "../../../../generated/entities.ids.generated.js";
10
10
  import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
11
11
  import { userCrudEvents, userCrudIndexer } from "@open-mercato/core/modules/auth/commands/users";
12
- import { assertActorCanGrantRoleTokens } from "@open-mercato/core/modules/auth/lib/grantChecks";
12
+ import {
13
+ assertActorCanGrantRoleTokens,
14
+ assertActorCanModifySuperAdminUserTarget,
15
+ listSuperAdminUserIds
16
+ } from "@open-mercato/core/modules/auth/lib/grantChecks";
13
17
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
14
18
  import { buildPasswordSchema } from "@open-mercato/shared/lib/auth/passwordPolicy";
15
19
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
@@ -17,6 +21,10 @@ import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
17
21
  import { tokenizeText } from "@open-mercato/shared/lib/search/tokenize";
18
22
  import { sql } from "kysely";
19
23
  import { normalizeDisplayNameInput } from "@open-mercato/core/modules/auth/lib/displayName";
24
+ import {
25
+ getSelectedTenantFromRequest,
26
+ resolveOrganizationScopeForRequest
27
+ } from "@open-mercato/core/modules/directory/utils/organizationScope";
20
28
  const querySchema = z.object({
21
29
  id: z.string().uuid().optional(),
22
30
  page: z.coerce.number().min(1).default(1),
@@ -109,6 +117,9 @@ const crud = makeCrudRoute({
109
117
  schema: rawBodySchema,
110
118
  mapInput: async ({ parsed, ctx }) => {
111
119
  if (ctx.request) {
120
+ if (typeof parsed.id === "string" && parsed.id.length) {
121
+ await assertCanModifySuperAdminTarget(ctx.request, parsed.id);
122
+ }
112
123
  await assertCanAssignRoles(ctx.request, parsed.roles, parsed);
113
124
  }
114
125
  return parsed;
@@ -138,12 +149,12 @@ async function GET(req) {
138
149
  if (!parsed.success) return NextResponse.json({ items: [], total: 0, totalPages: 1 });
139
150
  const container = await createRequestContainer();
140
151
  const em = container.resolve("em");
141
- let isSuperAdmin = false;
152
+ let isSuperAdmin = auth.isSuperAdmin === true;
142
153
  try {
143
154
  if (auth.sub) {
144
155
  const rbacService = container.resolve("rbacService");
145
156
  const acl = await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null });
146
- isSuperAdmin = !!acl?.isSuperAdmin;
157
+ isSuperAdmin = isSuperAdmin || !!acl?.isSuperAdmin;
147
158
  }
148
159
  } catch (err) {
149
160
  console.error("users: failed to resolve rbac", err);
@@ -151,16 +162,60 @@ async function GET(req) {
151
162
  const { id, page, pageSize, search, name, organizationId, roleIds } = parsed.data;
152
163
  const filters = [{ deletedAt: null }];
153
164
  const actorTenantId = auth.tenantId ? String(auth.tenantId) : null;
165
+ let effectiveTenantId = null;
166
+ let effectiveOrganizationIds = null;
167
+ let effectiveSelectedOrganizationId = null;
168
+ let usesSelectedTenantScope = false;
154
169
  if (!isSuperAdmin) {
155
170
  if (!actorTenantId) {
156
171
  return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin });
157
172
  }
158
- filters.push({ tenantId: actorTenantId });
173
+ effectiveTenantId = actorTenantId;
174
+ const superAdminUserIds = await listSuperAdminUserIds(em, actorTenantId);
175
+ if (superAdminUserIds.size) {
176
+ filters.push({ id: { $nin: Array.from(superAdminUserIds) } });
177
+ }
178
+ } else {
179
+ const selectedTenantId = getSelectedTenantFromRequest(req);
180
+ if (typeof selectedTenantId === "string" && selectedTenantId.trim().length > 0) {
181
+ const scope = await resolveOrganizationScopeForRequest({
182
+ container,
183
+ auth,
184
+ request: req,
185
+ tenantId: selectedTenantId.trim()
186
+ });
187
+ if (!scope.tenantId) {
188
+ return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin });
189
+ }
190
+ effectiveTenantId = scope.tenantId;
191
+ effectiveSelectedOrganizationId = scope.selectedId;
192
+ usesSelectedTenantScope = true;
193
+ if (Array.isArray(scope.filterIds)) {
194
+ if (scope.filterIds.length === 0) {
195
+ return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin });
196
+ }
197
+ effectiveOrganizationIds = scope.filterIds;
198
+ }
199
+ }
200
+ }
201
+ if (effectiveTenantId) {
202
+ filters.push({ tenantId: effectiveTenantId });
203
+ }
204
+ if (effectiveOrganizationIds) {
205
+ filters.push({ organizationId: { $in: effectiveOrganizationIds } });
159
206
  }
207
+ const scopeOrganizationId = usesSelectedTenantScope ? effectiveSelectedOrganizationId : auth.orgId ?? null;
160
208
  if (organizationId) filters.push({ organizationId });
161
209
  const trimmedName = typeof name === "string" ? name.trim() : "";
162
210
  if (trimmedName) {
163
- filters.push({ name: { $ilike: `%${escapeLikePattern(trimmedName)}%` } });
211
+ const searchPattern = `%${escapeLikePattern(trimmedName)}%`;
212
+ const displayNameFilters = [{ name: { $ilike: searchPattern } }];
213
+ const nameTokenScope = isSuperAdmin ? effectiveTenantId ?? void 0 : auth.tenantId ?? null;
214
+ const matchedDisplayNameIds = await findUserIdsBySearchTokens(em, E.auth.user, trimmedName, nameTokenScope, "name");
215
+ if (matchedDisplayNameIds && matchedDisplayNameIds.length) {
216
+ displayNameFilters.push({ id: { $in: matchedDisplayNameIds } });
217
+ }
218
+ filters.push(displayNameFilters.length > 1 ? { $or: displayNameFilters } : displayNameFilters[0]);
164
219
  }
165
220
  let idFilter = id ? /* @__PURE__ */ new Set([id]) : null;
166
221
  if (Array.isArray(roleIds) && roleIds.length > 0) {
@@ -183,7 +238,7 @@ async function GET(req) {
183
238
  }
184
239
  const trimmedSearch = typeof search === "string" ? search.trim() : "";
185
240
  if (trimmedSearch) {
186
- const tenantScope = isSuperAdmin ? void 0 : auth.tenantId ?? null;
241
+ const tenantScope = isSuperAdmin ? effectiveTenantId ?? void 0 : auth.tenantId ?? null;
187
242
  const searchFilters = [];
188
243
  const matchedIds = await findUserIdsBySearchTokens(em, E.auth.user, trimmedSearch, tenantScope);
189
244
  if (matchedIds && matchedIds.length) {
@@ -251,7 +306,10 @@ async function GET(req) {
251
306
  UserRole,
252
307
  { user: { $in: userIds } },
253
308
  { populate: ["role"] },
254
- { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null }
309
+ {
310
+ tenantId: effectiveTenantId ?? auth.tenantId ?? null,
311
+ organizationId: scopeOrganizationId
312
+ }
255
313
  ) : [];
256
314
  const roleMap = {};
257
315
  const roleIdMap = {};
@@ -311,7 +369,7 @@ async function GET(req) {
311
369
  recordIds: userIds.map(String),
312
370
  tenantIdByRecord: tenantByUser,
313
371
  organizationIdByRecord: organizationByUser,
314
- tenantFallbacks: auth.tenantId ? [auth.tenantId] : []
372
+ tenantFallbacks: effectiveTenantId ? [effectiveTenantId] : auth.tenantId ? [auth.tenantId] : []
315
373
  }) : {};
316
374
  const items = rows.map((u) => {
317
375
  const uid = String(u.id);
@@ -338,8 +396,8 @@ async function GET(req) {
338
396
  items,
339
397
  idField: "id",
340
398
  resourceKind: "auth.user",
341
- organizationId: null,
342
- tenantId: auth.tenantId ?? null,
399
+ organizationId: effectiveSelectedOrganizationId,
400
+ tenantId: effectiveTenantId ?? auth.tenantId ?? null,
343
401
  query: parsed.data,
344
402
  accessType: id ? "read:item" : void 0
345
403
  });
@@ -351,8 +409,21 @@ const POST = async (req) => {
351
409
  const PUT = async (req) => {
352
410
  return crud.PUT(req);
353
411
  };
354
- const DELETE = crud.DELETE;
355
- async function findUserIdsBySearchTokens(em, entityType, search, tenantScope) {
412
+ const DELETE = async (req) => {
413
+ const targetId = new URL(req.url).searchParams.get("id");
414
+ if (targetId) {
415
+ try {
416
+ await assertCanModifySuperAdminTarget(req, targetId);
417
+ } catch (err) {
418
+ if (err instanceof CrudHttpError) {
419
+ return NextResponse.json(err.body, { status: err.status });
420
+ }
421
+ throw err;
422
+ }
423
+ }
424
+ return crud.DELETE(req);
425
+ };
426
+ async function findUserIdsBySearchTokens(em, entityType, search, tenantScope, field) {
356
427
  const trimmed = search.trim();
357
428
  if (!trimmed) return null;
358
429
  const searchConfig = resolveSearchConfig();
@@ -361,12 +432,29 @@ async function findUserIdsBySearchTokens(em, entityType, search, tenantScope) {
361
432
  if (!hashes.length) return [];
362
433
  const db = em.getKysely();
363
434
  let query = db.selectFrom("search_tokens").select("entity_id").where("entity_type", "=", entityType).where("token_hash", "in", hashes).groupBy("entity_id").having(sql`count(distinct token_hash) >= ${hashes.length}`);
435
+ if (field) {
436
+ query = query.where("field", "=", field);
437
+ }
364
438
  if (tenantScope !== void 0) {
365
439
  query = query.where(sql`tenant_id is not distinct from ${tenantScope}`);
366
440
  }
367
441
  const rows = await query.execute();
368
442
  return rows.map((row) => typeof row.entity_id === "string" ? row.entity_id : null).filter((id) => typeof id === "string" && id.length > 0);
369
443
  }
444
+ async function assertCanModifySuperAdminTarget(req, targetUserId) {
445
+ const auth = await getAuthFromRequest(req);
446
+ if (!auth?.sub) throw new CrudHttpError(401, { error: "Unauthorized" });
447
+ const container = await createRequestContainer();
448
+ const em = container.resolve("em");
449
+ await assertActorCanModifySuperAdminUserTarget({
450
+ em,
451
+ rbacService: container.resolve("rbacService"),
452
+ actorUserId: auth.sub,
453
+ tenantId: auth.tenantId ?? null,
454
+ organizationId: auth.orgId ?? null,
455
+ targetUserId
456
+ });
457
+ }
370
458
  async function assertCanAssignRoles(req, roles, payload) {
371
459
  if (!Array.isArray(roles)) return;
372
460
  const auth = await getAuthFromRequest(req);
@@ -414,7 +502,7 @@ const openApi = {
414
502
  methods: {
415
503
  GET: {
416
504
  summary: "List users",
417
- description: "Returns users for the current tenant. Search matches email, organization name, and role name. Super administrators may scope the response via organization or role filters.",
505
+ description: "Returns users for the effective selected tenant and organization scope. Search matches email, organization name, and role name. Super administrators may scope the response via the topbar context, organization filters, or role filters.",
418
506
  query: querySchema,
419
507
  responses: [
420
508
  { status: 200, description: "User collection", schema: userListResponseSchema }