@open-mercato/core 0.6.8-develop.6944.1.eef6a0ee1d → 0.6.8-develop.6950.1.912d574d60

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 (51) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/authFixtures.js +3 -0
  3. package/dist/helpers/integration/authFixtures.js.map +2 -2
  4. package/dist/modules/auth/api/users/acl/route.js +15 -2
  5. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  6. package/dist/modules/auth/api/users/consents/route.js +8 -1
  7. package/dist/modules/auth/api/users/consents/route.js.map +2 -2
  8. package/dist/modules/auth/api/users/resend-invite/route.js +8 -1
  9. package/dist/modules/auth/api/users/resend-invite/route.js.map +2 -2
  10. package/dist/modules/auth/api/users/route.js +80 -6
  11. package/dist/modules/auth/api/users/route.js.map +2 -2
  12. package/dist/modules/auth/commands/users.js +49 -2
  13. package/dist/modules/auth/commands/users.js.map +2 -2
  14. package/dist/modules/auth/lib/grantChecks.js +86 -5
  15. package/dist/modules/auth/lib/grantChecks.js.map +2 -2
  16. package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
  17. package/dist/modules/customer_accounts/lib/customerAuth.js +19 -11
  18. package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
  19. package/dist/modules/customer_accounts/lib/customerAuthServer.js +14 -7
  20. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  21. package/dist/modules/customer_accounts/services/customerSessionService.js +14 -0
  22. package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
  23. package/dist/modules/customers/api/interactions/route.js +16 -5
  24. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  25. package/dist/modules/customers/components/calendar/CalendarScreen.js +5 -5
  26. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
  27. package/dist/modules/customers/components/calendar/editor/hooks.js +2 -2
  28. package/dist/modules/customers/components/calendar/editor/hooks.js.map +2 -2
  29. package/dist/modules/customers/components/calendar/useCalendarItems.js +33 -7
  30. package/dist/modules/customers/components/calendar/useCalendarItems.js.map +2 -2
  31. package/package.json +7 -7
  32. package/src/helpers/integration/authFixtures.ts +5 -2
  33. package/src/modules/auth/api/users/acl/route.ts +13 -0
  34. package/src/modules/auth/api/users/consents/route.ts +7 -0
  35. package/src/modules/auth/api/users/resend-invite/route.ts +7 -0
  36. package/src/modules/auth/api/users/route.ts +80 -4
  37. package/src/modules/auth/commands/users.ts +54 -2
  38. package/src/modules/auth/i18n/de.json +4 -0
  39. package/src/modules/auth/i18n/en.json +4 -0
  40. package/src/modules/auth/i18n/es.json +4 -0
  41. package/src/modules/auth/i18n/ko.json +4 -0
  42. package/src/modules/auth/i18n/pl.json +4 -0
  43. package/src/modules/auth/lib/grantChecks.ts +121 -5
  44. package/src/modules/auth/lib/sessionIntegrity.ts +5 -4
  45. package/src/modules/customer_accounts/lib/customerAuth.ts +31 -12
  46. package/src/modules/customer_accounts/lib/customerAuthServer.ts +19 -7
  47. package/src/modules/customer_accounts/services/customerSessionService.ts +20 -0
  48. package/src/modules/customers/api/interactions/route.ts +16 -5
  49. package/src/modules/customers/components/calendar/CalendarScreen.tsx +5 -5
  50. package/src/modules/customers/components/calendar/editor/hooks.ts +2 -2
  51. package/src/modules/customers/components/calendar/useCalendarItems.ts +43 -6
@@ -1,4 +1,4 @@
1
- [build:core] found 3941 entry points
1
+ [build:core] found 3948 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 204 entry points
4
4
  [build:core:generated] built successfully
@@ -50,6 +50,9 @@ async function createOrganizationFixture(request, token, input) {
50
50
  if (typeof input.tenantId === "string" && input.tenantId.length > 0) {
51
51
  payload.tenantId = input.tenantId;
52
52
  }
53
+ if (typeof input.parentId === "string" && input.parentId.length > 0) {
54
+ payload.parentId = input.parentId;
55
+ }
53
56
  const response = await apiRequest(request, "POST", "/api/directory/organizations", {
54
57
  token,
55
58
  data: payload
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/helpers/integration/authFixtures.ts"],
4
- "sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test';\nimport { apiRequest, getAuthToken } from './api';\nimport { expectId, readJsonSafe } from './generalFixtures';\n\n// Re-exported so tests can import auth helpers from a single fixtures module.\nexport { getAuthToken };\n\nconst BASE_URL = process.env.BASE_URL?.trim() || null;\n\nfunction resolveUrl(path: string): string {\n return BASE_URL ? `${BASE_URL}${path}` : path;\n}\n\n/**\n * Variant of {@link apiRequest} that sets the `om_selected_org` cookie so the\n * server resolves `ctx.selectedOrganizationId` to a specific organization.\n * Create routes scope new records to that organization, which lets a test place\n * a fixture in an organization other than the caller's home org.\n */\nexport async function apiRequestWithSelectedOrg(\n request: APIRequestContext,\n method: string,\n path: string,\n options: { token: string; selectedOrgId: string; data?: unknown },\n) {\n const headers = {\n Authorization: `Bearer ${options.token}`,\n 'Content-Type': 'application/json',\n Cookie: `om_selected_org=${options.selectedOrgId}`,\n };\n return request.fetch(resolveUrl(path), { method, headers, data: options.data });\n}\n\nexport async function createRoleFixture(\n request: APIRequestContext,\n token: string,\n input: { name: string; tenantId?: string },\n): Promise<string> {\n const payload: { name: string; tenantId?: string } = {\n name: input.name,\n };\n if (typeof input.tenantId === 'string' && input.tenantId.length > 0) {\n payload.tenantId = input.tenantId;\n }\n const response = await apiRequest(request, 'POST', '/api/auth/roles', {\n token,\n data: payload,\n });\n const body = await readJsonSafe<{ id?: string }>(response);\n expect(response.status(), 'POST /api/auth/roles should return 201').toBe(201);\n return expectId(body?.id, 'Role creation response should include id');\n}\n\nexport async function deleteRoleIfExists(\n request: APIRequestContext,\n token: string | null,\n roleId: string | null,\n): Promise<void> {\n if (!token || !roleId) return;\n await apiRequest(request, 'DELETE', `/api/auth/roles?id=${encodeURIComponent(roleId)}`, { token }).catch(() => undefined);\n}\n\nexport async function createUserFixture(\n request: APIRequestContext,\n token: string,\n input: { email: string; password: string; organizationId: string; roles: string[]; name?: string },\n): Promise<string> {\n const response = await apiRequest(request, 'POST', '/api/auth/users', {\n token,\n data: input,\n });\n const body = await readJsonSafe<{ id?: string }>(response);\n expect(response.status(), 'POST /api/auth/users should return 201').toBe(201);\n return expectId(body?.id, 'User creation response should include id');\n}\n\nexport async function deleteUserIfExists(\n request: APIRequestContext,\n token: string | null,\n userId: string | null,\n): Promise<void> {\n if (!token || !userId) return;\n await apiRequest(request, 'DELETE', `/api/auth/users?id=${encodeURIComponent(userId)}`, { token }).catch(() => undefined);\n}\n\nexport async function createOrganizationFixture(\n request: APIRequestContext,\n token: string,\n input: { name: string; tenantId?: string },\n): Promise<string> {\n const payload: { name: string; tenantId?: string } = { name: input.name };\n if (typeof input.tenantId === 'string' && input.tenantId.length > 0) {\n payload.tenantId = input.tenantId;\n }\n const response = await apiRequest(request, 'POST', '/api/directory/organizations', {\n token,\n data: payload,\n });\n const body = await readJsonSafe<{ id?: string }>(response);\n expect(response.status(), 'POST /api/directory/organizations should return 201').toBe(201);\n return expectId(body?.id, 'Organization creation response should include id');\n}\n\nexport async function deleteOrganizationIfExists(\n request: APIRequestContext,\n token: string | null,\n organizationId: string | null,\n): Promise<void> {\n if (!token || !organizationId) return;\n await apiRequest(request, 'DELETE', '/api/directory/organizations', {\n token,\n data: { id: organizationId },\n }).catch(() => undefined);\n}\n\nexport async function setRoleAclFeatures(\n request: APIRequestContext,\n token: string,\n input: { roleId: string; features: string[]; organizations?: string[] | null },\n): Promise<void> {\n const payload: { roleId: string; features: string[]; organizations?: string[] | null } = {\n roleId: input.roleId,\n features: input.features,\n };\n if (input.organizations !== undefined) {\n payload.organizations = input.organizations;\n }\n const response = await apiRequest(request, 'PUT', '/api/auth/roles/acl', {\n token,\n data: payload,\n });\n const body = await readJsonSafe<{ ok?: boolean }>(response);\n expect(response.status(), 'PUT /api/auth/roles/acl should return 200').toBe(200);\n expect(body?.ok, 'Role ACL update should report ok=true').toBe(true);\n}\n\nexport async function setUserAclVisibility(\n request: APIRequestContext,\n token: string,\n input: { userId: string; organizations: string[] | null; features?: string[] },\n): Promise<void> {\n const payload: { userId: string; organizations: string[] | null; features?: string[] } = {\n userId: input.userId,\n organizations: input.organizations,\n };\n if (input.features !== undefined) {\n payload.features = input.features;\n }\n const response = await apiRequest(request, 'PUT', '/api/auth/users/acl', {\n token,\n data: payload,\n });\n const body = await readJsonSafe<{ ok?: boolean }>(response);\n expect(response.status(), 'PUT /api/auth/users/acl should return 200').toBe(200);\n expect(body?.ok, 'User ACL update should report ok=true').toBe(true);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test';\nimport { apiRequest, getAuthToken } from './api';\nimport { expectId, readJsonSafe } from './generalFixtures';\n\n// Re-exported so tests can import auth helpers from a single fixtures module.\nexport { getAuthToken };\n\nconst BASE_URL = process.env.BASE_URL?.trim() || null;\n\nfunction resolveUrl(path: string): string {\n return BASE_URL ? `${BASE_URL}${path}` : path;\n}\n\n/**\n * Variant of {@link apiRequest} that sets the `om_selected_org` cookie so the\n * server resolves `ctx.selectedOrganizationId` to a specific organization.\n * Create routes scope new records to that organization, which lets a test place\n * a fixture in an organization other than the caller's home org.\n */\nexport async function apiRequestWithSelectedOrg(\n request: APIRequestContext,\n method: string,\n path: string,\n options: { token: string; selectedOrgId: string; data?: unknown },\n) {\n const headers = {\n Authorization: `Bearer ${options.token}`,\n 'Content-Type': 'application/json',\n Cookie: `om_selected_org=${options.selectedOrgId}`,\n };\n return request.fetch(resolveUrl(path), { method, headers, data: options.data });\n}\n\nexport async function createRoleFixture(\n request: APIRequestContext,\n token: string,\n input: { name: string; tenantId?: string },\n): Promise<string> {\n const payload: { name: string; tenantId?: string } = {\n name: input.name,\n };\n if (typeof input.tenantId === 'string' && input.tenantId.length > 0) {\n payload.tenantId = input.tenantId;\n }\n const response = await apiRequest(request, 'POST', '/api/auth/roles', {\n token,\n data: payload,\n });\n const body = await readJsonSafe<{ id?: string }>(response);\n expect(response.status(), 'POST /api/auth/roles should return 201').toBe(201);\n return expectId(body?.id, 'Role creation response should include id');\n}\n\nexport async function deleteRoleIfExists(\n request: APIRequestContext,\n token: string | null,\n roleId: string | null,\n): Promise<void> {\n if (!token || !roleId) return;\n await apiRequest(request, 'DELETE', `/api/auth/roles?id=${encodeURIComponent(roleId)}`, { token }).catch(() => undefined);\n}\n\nexport async function createUserFixture(\n request: APIRequestContext,\n token: string,\n input: { email: string; password: string; organizationId: string; roles: string[]; name?: string },\n): Promise<string> {\n const response = await apiRequest(request, 'POST', '/api/auth/users', {\n token,\n data: input,\n });\n const body = await readJsonSafe<{ id?: string }>(response);\n expect(response.status(), 'POST /api/auth/users should return 201').toBe(201);\n return expectId(body?.id, 'User creation response should include id');\n}\n\nexport async function deleteUserIfExists(\n request: APIRequestContext,\n token: string | null,\n userId: string | null,\n): Promise<void> {\n if (!token || !userId) return;\n await apiRequest(request, 'DELETE', `/api/auth/users?id=${encodeURIComponent(userId)}`, { token }).catch(() => undefined);\n}\n\nexport async function createOrganizationFixture(\n request: APIRequestContext,\n token: string,\n input: { name: string; tenantId?: string; parentId?: string },\n): Promise<string> {\n const payload: { name: string; tenantId?: string; parentId?: string } = { name: input.name };\n if (typeof input.tenantId === 'string' && input.tenantId.length > 0) {\n payload.tenantId = input.tenantId;\n }\n if (typeof input.parentId === 'string' && input.parentId.length > 0) {\n payload.parentId = input.parentId;\n }\n const response = await apiRequest(request, 'POST', '/api/directory/organizations', {\n token,\n data: payload,\n });\n const body = await readJsonSafe<{ id?: string }>(response);\n expect(response.status(), 'POST /api/directory/organizations should return 201').toBe(201);\n return expectId(body?.id, 'Organization creation response should include id');\n}\n\nexport async function deleteOrganizationIfExists(\n request: APIRequestContext,\n token: string | null,\n organizationId: string | null,\n): Promise<void> {\n if (!token || !organizationId) return;\n await apiRequest(request, 'DELETE', '/api/directory/organizations', {\n token,\n data: { id: organizationId },\n }).catch(() => undefined);\n}\n\nexport async function setRoleAclFeatures(\n request: APIRequestContext,\n token: string,\n input: { roleId: string; features: string[]; organizations?: string[] | null },\n): Promise<void> {\n const payload: { roleId: string; features: string[]; organizations?: string[] | null } = {\n roleId: input.roleId,\n features: input.features,\n };\n if (input.organizations !== undefined) {\n payload.organizations = input.organizations;\n }\n const response = await apiRequest(request, 'PUT', '/api/auth/roles/acl', {\n token,\n data: payload,\n });\n const body = await readJsonSafe<{ ok?: boolean }>(response);\n expect(response.status(), 'PUT /api/auth/roles/acl should return 200').toBe(200);\n expect(body?.ok, 'Role ACL update should report ok=true').toBe(true);\n}\n\nexport async function setUserAclVisibility(\n request: APIRequestContext,\n token: string,\n input: { userId: string; organizations: string[] | null; features?: string[] },\n): Promise<void> {\n const payload: { userId: string; organizations: string[] | null; features?: string[] } = {\n userId: input.userId,\n organizations: input.organizations,\n };\n if (input.features !== undefined) {\n payload.features = input.features;\n }\n const response = await apiRequest(request, 'PUT', '/api/auth/users/acl', {\n token,\n data: payload,\n });\n const body = await readJsonSafe<{ ok?: boolean }>(response);\n expect(response.status(), 'PUT /api/auth/users/acl should return 200').toBe(200);\n expect(body?.ok, 'User ACL update should report ok=true').toBe(true);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -13,6 +13,7 @@ import {
13
13
  assertActorCanModifySuperAdminUserTarget,
14
14
  normalizeGrantFeatureList
15
15
  } from "@open-mercato/core/modules/auth/lib/grantChecks";
16
+ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
16
17
  const getSchema = z.object({ userId: z.string().uuid() });
17
18
  const putSchema = z.object({
18
19
  userId: z.string().uuid(),
@@ -64,7 +65,13 @@ async function GET(req) {
64
65
  tenantId: auth.tenantId ?? null,
65
66
  organizationId: auth.orgId ?? null,
66
67
  targetUserId: parsed.data.userId,
67
- actorIsSuperAdmin: false
68
+ actorIsSuperAdmin: false,
69
+ organizationScope: await resolveOrganizationScopeForRequest({
70
+ container,
71
+ auth,
72
+ request: req,
73
+ tenantId: auth.tenantId ?? null
74
+ })
68
75
  });
69
76
  } catch (err) {
70
77
  if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
@@ -122,7 +129,13 @@ async function PUT(req) {
122
129
  tenantId: auth.tenantId ?? null,
123
130
  organizationId: auth.orgId ?? null,
124
131
  targetUserId: parsed.data.userId,
125
- actorIsSuperAdmin: false
132
+ actorIsSuperAdmin: false,
133
+ organizationScope: await resolveOrganizationScopeForRequest({
134
+ container,
135
+ auth,
136
+ request: req,
137
+ tenantId: auth.tenantId ?? null
138
+ })
126
139
  });
127
140
  } catch (err) {
128
141
  if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
@@ -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, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { UserAcl } from '@open-mercato/core/modules/auth/data/entities'\nimport {\n assertActorCanAccessUserTarget,\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminUserTarget,\n normalizeGrantFeatureList,\n} 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 updatedAt: 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 await assertActorCanAccessUserTarget({\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 updatedAt: acl.updatedAt instanceof Date ? acl.updatedAt.toISOString() : null,\n }\n : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null, updatedAt: 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 await assertActorCanAccessUserTarget({\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 = normalizeGrantFeatureList(parsed.data.features)\n const organizations = normalizeOrganizations(parsed.data.organizations)\n\n let acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n // Optimistic lock: refuse a stale per-user ACL overwrite so concurrent edits\n // cannot silently clobber each other (#2055). Strictly additive \u2014 a no-op when\n // the client sends no expected-version header; skipped when no ACL row exists.\n if (acl) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'auth.user_acl',\n resourceId: acl.id,\n current: acl.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false\n const existingFeatures = acl ? normalizeGrantFeatureList(acl.featuresJson) : []\n\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? false\n\n try {\n await assertActorCanGrantAcl({\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 isSuperAdmin: requestedIsSuperAdmin,\n features: requestedFeatures,\n organizations,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const effectiveFeatures = actorIsSuperAdmin\n ? requestedFeatures\n : sanitizeTenantFeatures(requestedFeatures)\n\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 // Persist the ACL mutation inside a transaction so the per-user permission\n // write (or removal) commits atomically (proper ACL-edit transaction handling).\n if (!hasCustomAcl) {\n if (acl) {\n const aclToRemove = acl\n await withAtomicFlush(em, [() => em.remove(aclToRemove)], { transaction: true })\n }\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 await withAtomicFlush(\n em,\n [\n () => {\n aclRecord.isSuperAdmin = effectiveIsSuperAdmin\n aclRecord.featuresJson = effectiveFeatures\n aclRecord.organizationsJson = organizations\n em.persist(aclRecord)\n },\n ],\n { transaction: true },\n )\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 normalizeOrganizations(organizations: unknown): string[] | null {\n if (!Array.isArray(organizations)) return null\n return normalizeGrantFeatureList(organizations)\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 { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { UserAcl } from '@open-mercato/core/modules/auth/data/entities'\nimport {\n assertActorCanAccessUserTarget,\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminUserTarget,\n normalizeGrantFeatureList,\n} 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'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\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 updatedAt: 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 await assertActorCanAccessUserTarget({\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 organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: auth.tenantId ?? null,\n }),\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 updatedAt: acl.updatedAt instanceof Date ? acl.updatedAt.toISOString() : null,\n }\n : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null, updatedAt: 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 await assertActorCanAccessUserTarget({\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 organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: auth.tenantId ?? null,\n }),\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 = normalizeGrantFeatureList(parsed.data.features)\n const organizations = normalizeOrganizations(parsed.data.organizations)\n\n let acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })\n // Optimistic lock: refuse a stale per-user ACL overwrite so concurrent edits\n // cannot silently clobber each other (#2055). Strictly additive \u2014 a no-op when\n // the client sends no expected-version header; skipped when no ACL row exists.\n if (acl) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'auth.user_acl',\n resourceId: acl.id,\n current: acl.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false\n const existingFeatures = acl ? normalizeGrantFeatureList(acl.featuresJson) : []\n\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? false\n\n try {\n await assertActorCanGrantAcl({\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 isSuperAdmin: requestedIsSuperAdmin,\n features: requestedFeatures,\n organizations,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const effectiveFeatures = actorIsSuperAdmin\n ? requestedFeatures\n : sanitizeTenantFeatures(requestedFeatures)\n\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 // Persist the ACL mutation inside a transaction so the per-user permission\n // write (or removal) commits atomically (proper ACL-edit transaction handling).\n if (!hasCustomAcl) {\n if (acl) {\n const aclToRemove = acl\n await withAtomicFlush(em, [() => em.remove(aclToRemove)], { transaction: true })\n }\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 await withAtomicFlush(\n em,\n [\n () => {\n aclRecord.isSuperAdmin = effectiveIsSuperAdmin\n aclRecord.featuresJson = effectiveFeatures\n aclRecord.organizationsJson = organizations\n em.persist(aclRecord)\n },\n ],\n { transaction: true },\n )\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 normalizeOrganizations(organizations: unknown): string[] | null {\n if (!Array.isArray(organizations)) return null\n return normalizeGrantFeatureList(organizations)\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
  }
@@ -7,6 +7,7 @@ import { verifyConsentIntegrityHash } from "@open-mercato/core/modules/auth/lib/
7
7
  import { assertActorCanAccessUserTarget } from "@open-mercato/core/modules/auth/lib/grantChecks";
8
8
  import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
9
9
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
10
+ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
10
11
  const metadata = {
11
12
  path: "/auth/users/consents",
12
13
  GET: {
@@ -39,7 +40,13 @@ async function GET(req) {
39
40
  actorUserId: auth.sub,
40
41
  tenantId,
41
42
  organizationId,
42
- targetUserId: parsed.data.userId
43
+ targetUserId: parsed.data.userId,
44
+ organizationScope: await resolveOrganizationScopeForRequest({
45
+ container,
46
+ auth,
47
+ request: req,
48
+ tenantId
49
+ })
43
50
  });
44
51
  } catch (err) {
45
52
  if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/users/consents/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { UserConsent } from '@open-mercato/core/modules/auth/data/entities'\nimport { verifyConsentIntegrityHash } from '@open-mercato/core/modules/auth/lib/consentIntegrity'\nimport { assertActorCanAccessUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { ConsentItem } from '@open-mercato/core/modules/auth/lib/consentTypes'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n path: '/auth/users/consents',\n GET: {\n requireAuth: true,\n requireFeatures: ['auth.users.edit'],\n },\n}\n\nconst querySchema = z.object({\n userId: z.string().uuid(),\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = querySchema.safeParse({ userId: url.searchParams.get('userId') })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid userId' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n if (auth.sub) {\n try {\n await assertActorCanAccessUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId,\n organizationId,\n targetUserId: parsed.data.userId,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const consents = await findWithDecryption(\n em,\n UserConsent,\n {\n userId: parsed.data.userId,\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n },\n { orderBy: { createdAt: 'DESC' } },\n { tenantId, organizationId },\n )\n\n const items: ConsentItem[] = consents.map((c) => ({\n id: c.id,\n consentType: c.consentType,\n isGranted: c.isGranted,\n grantedAt: c.grantedAt?.toISOString() ?? null,\n withdrawnAt: c.withdrawnAt?.toISOString() ?? null,\n source: c.source ?? null,\n ipAddress: c.ipAddress ?? null,\n integrityValid: verifyConsentIntegrityHash({\n userId: c.userId,\n consentType: c.consentType,\n isGranted: c.isGranted,\n grantedAt: c.grantedAt,\n withdrawnAt: c.withdrawnAt,\n ipAddress: c.ipAddress,\n source: c.source,\n }, c.integrityHash),\n createdAt: c.createdAt.toISOString(),\n updatedAt: c.updatedAt?.toISOString() ?? null,\n }))\n\n return NextResponse.json({ ok: true, items })\n}\n\nexport default GET\n\nconst consentsGetDoc: OpenApiMethodDoc = {\n summary: 'List user consents',\n description: 'Returns all consent records for a given user, with integrity verification status.',\n tags: ['Auth'],\n query: querySchema,\n responses: [\n { status: 200, description: 'Consent list returned' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Auth',\n summary: 'User consents',\n methods: {\n GET: consentsGetDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { UserConsent } from '@open-mercato/core/modules/auth/data/entities'\nimport { verifyConsentIntegrityHash } from '@open-mercato/core/modules/auth/lib/consentIntegrity'\nimport { assertActorCanAccessUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { ConsentItem } from '@open-mercato/core/modules/auth/lib/consentTypes'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nexport const metadata = {\n path: '/auth/users/consents',\n GET: {\n requireAuth: true,\n requireFeatures: ['auth.users.edit'],\n },\n}\n\nconst querySchema = z.object({\n userId: z.string().uuid(),\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = querySchema.safeParse({ userId: url.searchParams.get('userId') })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid userId' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n if (auth.sub) {\n try {\n await assertActorCanAccessUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId,\n organizationId,\n targetUserId: parsed.data.userId,\n organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId,\n }),\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const consents = await findWithDecryption(\n em,\n UserConsent,\n {\n userId: parsed.data.userId,\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n },\n { orderBy: { createdAt: 'DESC' } },\n { tenantId, organizationId },\n )\n\n const items: ConsentItem[] = consents.map((c) => ({\n id: c.id,\n consentType: c.consentType,\n isGranted: c.isGranted,\n grantedAt: c.grantedAt?.toISOString() ?? null,\n withdrawnAt: c.withdrawnAt?.toISOString() ?? null,\n source: c.source ?? null,\n ipAddress: c.ipAddress ?? null,\n integrityValid: verifyConsentIntegrityHash({\n userId: c.userId,\n consentType: c.consentType,\n isGranted: c.isGranted,\n grantedAt: c.grantedAt,\n withdrawnAt: c.withdrawnAt,\n ipAddress: c.ipAddress,\n source: c.source,\n }, c.integrityHash),\n createdAt: c.createdAt.toISOString(),\n updatedAt: c.updatedAt?.toISOString() ?? null,\n }))\n\n return NextResponse.json({ ok: true, items })\n}\n\nexport default GET\n\nconst consentsGetDoc: OpenApiMethodDoc = {\n summary: 'List user consents',\n description: 'Returns all consent records for a given user, with integrity verification status.',\n tags: ['Auth'],\n query: querySchema,\n responses: [\n { status: 200, description: 'Consent list returned' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Auth',\n summary: 'User consents',\n methods: {\n GET: consentsGetDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -15,6 +15,7 @@ import { INVITE_TOKEN_TTL_MS } from "@open-mercato/core/modules/auth/lib/inviteT
15
15
  import { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from "@open-mercato/shared/lib/url";
16
16
  import { generateAuthToken, hashAuthToken } from "@open-mercato/core/modules/auth/lib/tokenHash";
17
17
  import { assertActorCanAccessUserTarget } from "@open-mercato/core/modules/auth/lib/grantChecks";
18
+ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
18
19
  import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
19
20
  import { createLogger } from "@open-mercato/shared/lib/logger";
20
21
  const logger = createLogger("auth").child({ component: "users-resend-invite" });
@@ -80,7 +81,13 @@ async function POST(req) {
80
81
  tenantId: auth.tenantId ?? null,
81
82
  organizationId: auth.orgId ?? null,
82
83
  targetUserId: parsed.data.id,
83
- actorIsSuperAdmin: isSuperAdmin
84
+ actorIsSuperAdmin: isSuperAdmin,
85
+ organizationScope: await resolveOrganizationScopeForRequest({
86
+ container,
87
+ auth,
88
+ request: req,
89
+ tenantId: auth.tenantId ?? null
90
+ })
84
91
  });
85
92
  } catch (err) {
86
93
  if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/users/resend-invite/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 { User, PasswordReset } from '@open-mercato/core/modules/auth/data/entities'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport InviteUserEmail from '@open-mercato/core/modules/auth/emails/InviteUserEmail'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { INVITE_TOKEN_TTL_MS } from '@open-mercato/core/modules/auth/lib/inviteToken'\nimport { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'\nimport { generateAuthToken, hashAuthToken } from '@open-mercato/core/modules/auth/lib/tokenHash'\nimport { assertActorCanAccessUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'users-resend-invite' })\n\nconst resendInviteRateLimitConfig = readEndpointRateLimitConfig('RESEND_INVITE', {\n points: 3, duration: 300, blockDuration: 300, keyPrefix: 'resend-invite',\n})\n\nconst requestSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nconst validationErrorSchema = z.object({\n error: z.string(),\n fieldErrors: z.record(z.string(), z.array(z.string())).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['auth.users.create'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: resendInviteRateLimitConfig,\n })\n if (rateLimitError) return rateLimitError\n\n const body = await readJsonSafe(req, {})\n const parsed = requestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Validation failed', fieldErrors: parsed.error.flatten().fieldErrors },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n let isSuperAdmin = false\n try {\n if (auth.sub) {\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (userId: string, scope: { tenantId: string | null; organizationId: string | null }) => Promise<{ isSuperAdmin?: boolean } | null>\n }\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n })\n isSuperAdmin = !!acl?.isSuperAdmin\n }\n } catch (err) {\n logger.error('Failed to resolve rbac', { err })\n }\n\n if (auth.sub) {\n try {\n await assertActorCanAccessUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.id,\n actorIsSuperAdmin: isSuperAdmin,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const where: Record<string, unknown> = { id: parsed.data.id, deletedAt: null }\n if (!isSuperAdmin) {\n if (!auth.tenantId) return NextResponse.json({ error: 'User not found' }, { status: 404 })\n where.tenantId = auth.tenantId\n }\n\n const user = await em.findOne(User, where as any)\n if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 })\n\n if (user.passwordHash) {\n return NextResponse.json({ error: 'User already has a password' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: user.tenantId ? String(user.tenantId) : auth.tenantId ?? '',\n organizationId: user.organizationId ? String(user.organizationId) : null,\n userId: auth.sub ?? '',\n resourceKind: 'auth.user',\n resourceId: String(user.id),\n operation: 'custom',\n requestMethod: 'POST',\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let base: string\n try {\n base = getSecurityEmailBaseUrl(req.url)\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'auth.users.resend-invite',\n configMessage: 'Invitation email is not configured',\n })\n if (mapped) return NextResponse.json(mapped.body, { status: mapped.status })\n throw error\n }\n\n await em.nativeUpdate(\n PasswordReset,\n { user: user.id, usedAt: null } as any,\n { usedAt: new Date() },\n )\n\n const rawToken = generateAuthToken()\n const tokenHash = hashAuthToken(rawToken)\n const expiresAt = new Date(Date.now() + INVITE_TOKEN_TTL_MS)\n const row = em.create(PasswordReset, { user, token: tokenHash, expiresAt, createdAt: new Date() })\n await em.persist(row).flush()\n\n const inviteUrl = `${base}/reset/${rawToken}`\n\n const { translate } = await resolveTranslations()\n const subject = translate('auth.email.invite.subject', 'You have been invited')\n const copy = {\n preview: translate('auth.email.invite.preview', 'Set up your account'),\n title: translate('auth.email.invite.title', 'You have been invited'),\n body: translate('auth.email.invite.body', 'An administrator has created an account for you. Click the link below to set your password. This link will expire in 48 hours.'),\n cta: translate('auth.email.invite.cta', 'Set up your password'),\n hint: translate('auth.email.invite.hint', 'If you did not expect this invitation, you can safely ignore this email.'),\n }\n\n let emailSent = true\n try {\n await sendEmail({ to: user.email, subject, react: InviteUserEmail({ inviteUrl, copy }) })\n } catch (err) {\n logger.error('Failed to send invitation email', { err })\n emailSent = false\n }\n\n if (guardResult?.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: user.tenantId ? String(user.tenantId) : auth.tenantId ?? '',\n organizationId: user.organizationId ? String(user.organizationId) : null,\n userId: auth.sub ?? '',\n resourceKind: 'auth.user',\n resourceId: String(user.id),\n operation: 'custom',\n requestMethod: 'POST',\n requestHeaders: req.headers,\n metadata: guardResult.metadata,\n })\n }\n\n if (!emailSent) {\n return NextResponse.json({ ok: true, warning: 'invite_email_failed' })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Resend user invite',\n methods: {\n POST: {\n summary: 'Resend invitation email',\n description: 'Resends the invitation email to a user who has not yet set up their password. Generates a new 48-hour setup token and invalidates prior tokens.',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [\n { status: 200, description: 'Invite email sent', schema: responseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request origin', schema: errorSchema },\n { status: 404, description: 'User not found', schema: errorSchema },\n { status: 409, description: 'User already has a password', schema: errorSchema },\n { status: 422, description: 'Validation error', schema: validationErrorSchema },\n { status: 429, description: 'Rate limit exceeded', schema: rateLimitErrorSchema },\n { status: 500, description: 'Invitation email origin is not configured', schema: errorSchema },\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 { User, PasswordReset } from '@open-mercato/core/modules/auth/data/entities'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport InviteUserEmail from '@open-mercato/core/modules/auth/emails/InviteUserEmail'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { INVITE_TOKEN_TTL_MS } from '@open-mercato/core/modules/auth/lib/inviteToken'\nimport { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'\nimport { generateAuthToken, hashAuthToken } from '@open-mercato/core/modules/auth/lib/tokenHash'\nimport { assertActorCanAccessUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'users-resend-invite' })\n\nconst resendInviteRateLimitConfig = readEndpointRateLimitConfig('RESEND_INVITE', {\n points: 3, duration: 300, blockDuration: 300, keyPrefix: 'resend-invite',\n})\n\nconst requestSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nconst validationErrorSchema = z.object({\n error: z.string(),\n fieldErrors: z.record(z.string(), z.array(z.string())).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['auth.users.create'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: resendInviteRateLimitConfig,\n })\n if (rateLimitError) return rateLimitError\n\n const body = await readJsonSafe(req, {})\n const parsed = requestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Validation failed', fieldErrors: parsed.error.flatten().fieldErrors },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n let isSuperAdmin = false\n try {\n if (auth.sub) {\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (userId: string, scope: { tenantId: string | null; organizationId: string | null }) => Promise<{ isSuperAdmin?: boolean } | null>\n }\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n })\n isSuperAdmin = !!acl?.isSuperAdmin\n }\n } catch (err) {\n logger.error('Failed to resolve rbac', { err })\n }\n\n if (auth.sub) {\n try {\n await assertActorCanAccessUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.id,\n actorIsSuperAdmin: isSuperAdmin,\n organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: auth.tenantId ?? null,\n }),\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const where: Record<string, unknown> = { id: parsed.data.id, deletedAt: null }\n if (!isSuperAdmin) {\n if (!auth.tenantId) return NextResponse.json({ error: 'User not found' }, { status: 404 })\n where.tenantId = auth.tenantId\n }\n\n const user = await em.findOne(User, where as any)\n if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 })\n\n if (user.passwordHash) {\n return NextResponse.json({ error: 'User already has a password' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: user.tenantId ? String(user.tenantId) : auth.tenantId ?? '',\n organizationId: user.organizationId ? String(user.organizationId) : null,\n userId: auth.sub ?? '',\n resourceKind: 'auth.user',\n resourceId: String(user.id),\n operation: 'custom',\n requestMethod: 'POST',\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let base: string\n try {\n base = getSecurityEmailBaseUrl(req.url)\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'auth.users.resend-invite',\n configMessage: 'Invitation email is not configured',\n })\n if (mapped) return NextResponse.json(mapped.body, { status: mapped.status })\n throw error\n }\n\n await em.nativeUpdate(\n PasswordReset,\n { user: user.id, usedAt: null } as any,\n { usedAt: new Date() },\n )\n\n const rawToken = generateAuthToken()\n const tokenHash = hashAuthToken(rawToken)\n const expiresAt = new Date(Date.now() + INVITE_TOKEN_TTL_MS)\n const row = em.create(PasswordReset, { user, token: tokenHash, expiresAt, createdAt: new Date() })\n await em.persist(row).flush()\n\n const inviteUrl = `${base}/reset/${rawToken}`\n\n const { translate } = await resolveTranslations()\n const subject = translate('auth.email.invite.subject', 'You have been invited')\n const copy = {\n preview: translate('auth.email.invite.preview', 'Set up your account'),\n title: translate('auth.email.invite.title', 'You have been invited'),\n body: translate('auth.email.invite.body', 'An administrator has created an account for you. Click the link below to set your password. This link will expire in 48 hours.'),\n cta: translate('auth.email.invite.cta', 'Set up your password'),\n hint: translate('auth.email.invite.hint', 'If you did not expect this invitation, you can safely ignore this email.'),\n }\n\n let emailSent = true\n try {\n await sendEmail({ to: user.email, subject, react: InviteUserEmail({ inviteUrl, copy }) })\n } catch (err) {\n logger.error('Failed to send invitation email', { err })\n emailSent = false\n }\n\n if (guardResult?.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: user.tenantId ? String(user.tenantId) : auth.tenantId ?? '',\n organizationId: user.organizationId ? String(user.organizationId) : null,\n userId: auth.sub ?? '',\n resourceKind: 'auth.user',\n resourceId: String(user.id),\n operation: 'custom',\n requestMethod: 'POST',\n requestHeaders: req.headers,\n metadata: guardResult.metadata,\n })\n }\n\n if (!emailSent) {\n return NextResponse.json({ ok: true, warning: 'invite_email_failed' })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Resend user invite',\n methods: {\n POST: {\n summary: 'Resend invitation email',\n description: 'Resends the invitation email to a user who has not yet set up their password. Generates a new 48-hour setup token and invalidates prior tokens.',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [\n { status: 200, description: 'Invite email sent', schema: responseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request origin', schema: errorSchema },\n { status: 404, description: 'User not found', schema: errorSchema },\n { status: 409, description: 'User already has a password', schema: errorSchema },\n { status: 422, description: 'Validation error', schema: validationErrorSchema },\n { status: 429, description: 'Rate limit exceeded', schema: rateLimitErrorSchema },\n { status: 500, description: 'Invitation email origin is not configured', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -11,9 +11,12 @@ import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fiel
11
11
  import { userCrudEvents, userCrudIndexer } from "@open-mercato/core/modules/auth/commands/users";
12
12
  import {
13
13
  assertActorCanAccessUserTarget,
14
+ assertActorCanAssignUserDestination,
14
15
  assertActorCanGrantRoleTokens,
15
16
  assertActorCanModifySuperAdminUserTarget,
16
- listSuperAdminUserIds
17
+ listSuperAdminUserIds,
18
+ resolveUserDestinationRoles,
19
+ throwUserDestinationOrganizationNotFound
17
20
  } from "@open-mercato/core/modules/auth/lib/grantChecks";
18
21
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
19
22
  import { buildPasswordSchema } from "@open-mercato/shared/lib/auth/passwordPolicy";
@@ -136,7 +139,14 @@ const crud = makeCrudRoute({
136
139
  await assertCanModifySuperAdminTarget(ctx.request, parsed.id);
137
140
  await assertCanAccessUserTarget(ctx.request, parsed.id);
138
141
  }
139
- await assertCanAssignRoles(ctx.request, parsed.roles, parsed);
142
+ if (typeof parsed.organizationId === "string" && parsed.organizationId.length) {
143
+ const destinationChanged = await assertCanAssignUserDestination(ctx.request, parsed);
144
+ if (!destinationChanged) {
145
+ await assertCanAssignRoles(ctx.request, parsed.roles, parsed);
146
+ }
147
+ } else {
148
+ await assertCanAssignRoles(ctx.request, parsed.roles, parsed);
149
+ }
140
150
  }
141
151
  return parsed;
142
152
  },
@@ -483,13 +493,20 @@ async function assertCanAccessUserTarget(req, targetUserId) {
483
493
  if (!auth?.sub) throw new CrudHttpError(401, { error: "Unauthorized" });
484
494
  const container = await createRequestContainer();
485
495
  const em = container.resolve("em");
496
+ const organizationScope = await resolveOrganizationScopeForRequest({
497
+ container,
498
+ auth,
499
+ request: req,
500
+ tenantId: auth.tenantId ?? null
501
+ });
486
502
  await assertActorCanAccessUserTarget({
487
503
  em,
488
504
  rbacService: container.resolve("rbacService"),
489
505
  actorUserId: auth.sub,
490
506
  tenantId: auth.tenantId ?? null,
491
507
  organizationId: auth.orgId ?? null,
492
- targetUserId
508
+ targetUserId,
509
+ organizationScope
493
510
  });
494
511
  }
495
512
  function resolveDeleteTargetId(parsed, raw) {
@@ -518,6 +535,63 @@ async function assertCanAssignRoles(req, roles, payload) {
518
535
  roleTokens: roles
519
536
  });
520
537
  }
538
+ async function assertCanAssignUserDestination(req, payload) {
539
+ const organizationId = typeof payload.organizationId === "string" ? payload.organizationId : null;
540
+ const targetUserId = typeof payload.id === "string" ? payload.id : null;
541
+ if (!organizationId || !targetUserId) return false;
542
+ const auth = await getAuthFromRequest(req);
543
+ if (!auth?.sub) throw new CrudHttpError(401, { error: "Unauthorized" });
544
+ const container = await createRequestContainer();
545
+ const em = container.resolve("em");
546
+ const targetUser = await findOneWithDecryption(
547
+ em,
548
+ User,
549
+ { id: targetUserId, deletedAt: null },
550
+ {},
551
+ { tenantId: null, organizationId: null }
552
+ );
553
+ if (!targetUser) return false;
554
+ const organization = await findOneWithDecryption(
555
+ em,
556
+ Organization,
557
+ { id: organizationId },
558
+ { populate: ["tenant"] },
559
+ { tenantId: null, organizationId }
560
+ );
561
+ if (!organization) return throwUserDestinationOrganizationNotFound(400);
562
+ const destinationTenantId = organization.tenant?.id ? String(organization.tenant.id) : null;
563
+ if (!destinationTenantId) return throwUserDestinationOrganizationNotFound(400);
564
+ const currentOrganizationId = targetUser.organizationId ? String(targetUser.organizationId) : null;
565
+ const currentTenantId = targetUser.tenantId ? String(targetUser.tenantId) : null;
566
+ if (currentOrganizationId === organizationId && currentTenantId === destinationTenantId) {
567
+ return false;
568
+ }
569
+ const roles = await resolveUserDestinationRoles({
570
+ em,
571
+ targetUserId,
572
+ destinationTenantId,
573
+ roleTokens: payload.roles
574
+ });
575
+ const organizationScope = await resolveOrganizationScopeForRequest({
576
+ container,
577
+ auth,
578
+ request: req,
579
+ tenantId: destinationTenantId
580
+ });
581
+ await assertActorCanAssignUserDestination({
582
+ em,
583
+ rbacService: container.resolve("rbacService"),
584
+ actorUserId: auth.sub,
585
+ actorIsSuperAdmin: auth.isSuperAdmin === true,
586
+ tenantId: auth.tenantId ?? null,
587
+ organizationId: auth.orgId ?? null,
588
+ allowedOrganizationIds: organizationScope.allowedIds,
589
+ destinationTenantId,
590
+ destinationOrganizationId: organizationId,
591
+ roles
592
+ });
593
+ return true;
594
+ }
521
595
  async function resolveTargetTenantIdForRoleGrant(em, payload, fallbackTenantId) {
522
596
  const organizationId = typeof payload.organizationId === "string" ? payload.organizationId : null;
523
597
  if (organizationId) {
@@ -577,7 +651,7 @@ const openApi = {
577
651
  },
578
652
  PUT: {
579
653
  summary: "Update user",
580
- description: "Updates profile fields including display name, organization assignment, credentials, or role memberships. Setting isConfirmed=false deactivates the account: the user can no longer sign in and every active session is revoked; isConfirmed=true reactivates it. A tenant cannot drop below a protected role's minimum active holder count, so revoking the role from, deactivating, moving, or deleting the last active administrator is rejected.",
654
+ description: "Updates profile fields including display name, organization assignment, credentials, or role memberships. A destination organization must be within the caller's descendant-expanded organization scope. Retained and newly assigned roles must belong to the destination tenant and be grantable by the caller. Setting isConfirmed=false deactivates the account: the user can no longer sign in and every active session is revoked; isConfirmed=true reactivates it. A tenant cannot drop below a protected role's minimum active holder count, so revoking the role from, deactivating, moving, or deleting the last active administrator is rejected.",
581
655
  requestBody: {
582
656
  contentType: "application/json",
583
657
  schema: userUpdateSchema
@@ -588,8 +662,8 @@ const openApi = {
588
662
  errors: [
589
663
  { status: 400, description: "Invalid payload, duplicate email, or the update would remove the last active holder of a protected role", schema: errorResponseSchema },
590
664
  { status: 401, description: "Unauthorized", schema: errorResponseSchema },
591
- { status: 403, description: "Attempted to assign privileged roles", schema: errorResponseSchema },
592
- { status: 404, description: "User not found", schema: errorResponseSchema }
665
+ { status: 403, description: "Destination organization is outside caller scope, or a retained or assigned role is not grantable", schema: errorResponseSchema },
666
+ { status: 404, description: "User or destination organization not found in the caller tenant scope", schema: errorResponseSchema }
593
667
  ]
594
668
  },
595
669
  DELETE: {