@open-mercato/core 0.10.1-develop.7836.1.0ef0f453c6 → 0.10.1-develop.7841.1.af87ac614d
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customer_accounts/api/admin/roles.js +25 -2
- package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users.js +24 -7
- package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +77 -15
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +12 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +2 -1
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/lib/adminOrganizationScope.js +46 -0
- package/dist/modules/customer_accounts/lib/adminOrganizationScope.js.map +7 -0
- package/dist/modules/customers/api/customer-dictionary-entries/route.js +86 -0
- package/dist/modules/customers/api/customer-dictionary-entries/route.js.map +7 -0
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +29 -7
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/cli.js +28 -110
- package/dist/modules/customers/cli.js.map +3 -3
- package/dist/modules/customers/components/DictionarySettings.js +1 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +1 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js +1 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +14 -1
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskForm.js +1 -16
- package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +38 -10
- package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js +12 -5
- package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useInteractions.js +3 -13
- package/dist/modules/customers/components/detail/hooks/useInteractions.js.map +2 -2
- package/dist/modules/customers/lib/dictionaryDefaults.js +134 -0
- package/dist/modules/customers/lib/dictionaryDefaults.js.map +7 -0
- package/dist/modules/customers/lib/dictionaryLabels.js +59 -0
- package/dist/modules/customers/lib/dictionaryLabels.js.map +7 -0
- package/dist/modules/customers/lib/interactionCompatibility.js +1 -0
- package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
- package/dist/modules/customers/translations.js +9 -0
- package/dist/modules/customers/translations.js.map +7 -0
- package/dist/modules/eudr/index.js +13 -0
- package/dist/modules/eudr/index.js.map +2 -2
- package/dist/modules/staff/api/timesheets/access-requests/route.js +11 -5
- package/dist/modules/staff/api/timesheets/access-requests/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +19 -12
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +21 -12
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +14 -9
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +31 -10
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js +64 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js +13 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js.map +7 -0
- package/dist/modules/sync_excel/index.js +11 -1
- package/dist/modules/sync_excel/index.js.map +2 -2
- package/dist/modules/translations/di.js +2 -0
- package/dist/modules/translations/di.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/api/admin/roles.ts +26 -2
- package/src/modules/customer_accounts/api/admin/users.ts +26 -7
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +96 -13
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +9 -1
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/i18n/de.json +10 -0
- package/src/modules/customer_accounts/i18n/en.json +10 -0
- package/src/modules/customer_accounts/i18n/es.json +10 -0
- package/src/modules/customer_accounts/i18n/ko.json +10 -0
- package/src/modules/customer_accounts/i18n/pl.json +10 -0
- package/src/modules/customer_accounts/lib/adminOrganizationScope.ts +71 -0
- package/src/modules/customers/api/customer-dictionary-entries/route.ts +82 -0
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +29 -6
- package/src/modules/customers/cli.ts +23 -124
- package/src/modules/customers/components/DictionarySettings.tsx +1 -1
- package/src/modules/customers/components/detail/ManageTagsDialog.tsx +1 -1
- package/src/modules/customers/components/detail/TaskDialog.tsx +15 -1
- package/src/modules/customers/components/detail/TaskForm.tsx +1 -17
- package/src/modules/customers/components/detail/TasksSection.tsx +44 -14
- package/src/modules/customers/components/detail/hooks/useCustomerDictionary.ts +14 -8
- package/src/modules/customers/components/detail/hooks/useInteractions.ts +3 -4
- package/src/modules/customers/i18n/de.json +81 -0
- package/src/modules/customers/i18n/en.json +81 -0
- package/src/modules/customers/i18n/es.json +81 -0
- package/src/modules/customers/i18n/ko.json +81 -0
- package/src/modules/customers/i18n/pl.json +81 -0
- package/src/modules/customers/lib/dictionaryDefaults.ts +137 -0
- package/src/modules/customers/lib/dictionaryLabels.ts +73 -0
- package/src/modules/customers/lib/interactionCompatibility.ts +1 -0
- package/src/modules/customers/translations.ts +5 -0
- package/src/modules/eudr/index.ts +13 -0
- package/src/modules/staff/api/timesheets/access-requests/route.ts +13 -14
- package/src/modules/staff/api/timesheets/time-projects/route.ts +22 -13
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +25 -14
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +14 -13
- package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +30 -8
- package/src/modules/staff/lib/time-tracking-ui/projectAccessScope.ts +102 -0
- package/src/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.ts +15 -0
- package/src/modules/sync_excel/index.ts +10 -0
- package/src/modules/translations/di.ts +2 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
5
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
@@ -6,6 +7,7 @@ import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern
|
|
|
6
7
|
import { CustomerRole, CustomerRoleAcl } from "@open-mercato/core/modules/customer_accounts/data/entities";
|
|
7
8
|
import { createRoleSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
|
|
8
9
|
import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
|
|
10
|
+
import { resolveAdminTargetOrganization } from "@open-mercato/core/modules/customer_accounts/lib/adminOrganizationScope";
|
|
9
11
|
const metadata = {};
|
|
10
12
|
async function GET(req) {
|
|
11
13
|
const auth = await getAuthFromRequest(req);
|
|
@@ -19,13 +21,27 @@ async function GET(req) {
|
|
|
19
21
|
return NextResponse.json({ ok: false, error: "Insufficient permissions" }, { status: 403 });
|
|
20
22
|
}
|
|
21
23
|
const url = new URL(req.url);
|
|
24
|
+
const requestedOrganizationId = url.searchParams.get("organizationId");
|
|
25
|
+
let organizationId = auth.orgId;
|
|
26
|
+
if (requestedOrganizationId) {
|
|
27
|
+
const target = await resolveAdminTargetOrganization({ container, auth, request: req, requestedOrganizationId });
|
|
28
|
+
if (!target.ok) return target.response;
|
|
29
|
+
organizationId = target.organizationId;
|
|
30
|
+
if (organizationId !== auth.orgId) {
|
|
31
|
+
const hasTargetAccess = await rbacService.userHasAllFeatures(auth.sub, ["customer_accounts.view"], { tenantId: auth.tenantId, organizationId });
|
|
32
|
+
if (!hasTargetAccess) {
|
|
33
|
+
const { translate } = await resolveTranslations();
|
|
34
|
+
return NextResponse.json({ ok: false, error: translate("customer_accounts.admin.errors.insufficientPermissions", "Insufficient permissions") }, { status: 403 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
22
38
|
const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10) || 1);
|
|
23
39
|
const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get("pageSize") || "50", 10) || 50));
|
|
24
40
|
const search = (url.searchParams.get("search") || "").trim();
|
|
25
41
|
const em = container.resolve("em");
|
|
26
42
|
const where = {
|
|
27
43
|
tenantId: auth.tenantId,
|
|
28
|
-
organizationId
|
|
44
|
+
organizationId,
|
|
29
45
|
deletedAt: null
|
|
30
46
|
};
|
|
31
47
|
if (search) {
|
|
@@ -151,14 +167,21 @@ const roleResponseSchema = z.object({
|
|
|
151
167
|
const errorSchema = z.object({ ok: z.literal(false), error: z.string() });
|
|
152
168
|
const getMethodDoc = {
|
|
153
169
|
summary: "List customer roles (admin)",
|
|
154
|
-
description: "Returns
|
|
170
|
+
description: "Returns customer roles for the caller's active organization, or for the organization passed as organizationId when it is within the caller's organization scope.",
|
|
155
171
|
tags: ["Customer Accounts Admin"],
|
|
172
|
+
query: z.object({
|
|
173
|
+
page: z.number().int().positive().optional(),
|
|
174
|
+
pageSize: z.number().int().positive().max(100).optional(),
|
|
175
|
+
search: z.string().optional(),
|
|
176
|
+
organizationId: z.string().uuid().optional()
|
|
177
|
+
}),
|
|
156
178
|
responses: [{
|
|
157
179
|
status: 200,
|
|
158
180
|
description: "Role list",
|
|
159
181
|
schema: z.object({ ok: z.literal(true), items: z.array(roleSchema), total: z.number(), totalPages: z.number(), page: z.number() })
|
|
160
182
|
}],
|
|
161
183
|
errors: [
|
|
184
|
+
{ status: 400, description: "Organization not found or outside the caller scope", schema: errorSchema },
|
|
162
185
|
{ status: 401, description: "Not authenticated", schema: errorSchema },
|
|
163
186
|
{ status: 403, description: "Insufficient permissions", schema: errorSchema }
|
|
164
187
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/admin/roles.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } 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 { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { createRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\n\nexport const metadata = {}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.view'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10) || 1)\n const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get('pageSize') || '50', 10) || 50))\n const search = (url.searchParams.get('search') || '').trim()\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const where: Record<string, unknown> = {\n tenantId: auth.tenantId,\n organizationId
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,yBAAyB;AAElC,SAAS,cAAc,uBAAuB;AAC9C,SAAS,wBAAwB;AACjC,SAAS,iCAAiC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } 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 { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { createRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { resolveAdminTargetOrganization } from '@open-mercato/core/modules/customer_accounts/lib/adminOrganizationScope'\n\nexport const metadata = {}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.view'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const requestedOrganizationId = url.searchParams.get('organizationId')\n let organizationId = auth.orgId\n if (requestedOrganizationId) {\n const target = await resolveAdminTargetOrganization({ container, auth, request: req, requestedOrganizationId })\n if (!target.ok) return target.response\n organizationId = target.organizationId\n if (organizationId !== auth.orgId) {\n const hasTargetAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.view'], { tenantId: auth.tenantId, organizationId })\n if (!hasTargetAccess) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({ ok: false, error: translate('customer_accounts.admin.errors.insufficientPermissions', 'Insufficient permissions') }, { status: 403 })\n }\n }\n }\n\n const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10) || 1)\n const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get('pageSize') || '50', 10) || 50))\n const search = (url.searchParams.get('search') || '').trim()\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const where: Record<string, unknown> = {\n tenantId: auth.tenantId,\n organizationId,\n deletedAt: null,\n }\n\n if (search) {\n const escapedSearch = escapeLikePattern(search)\n where.$or = [\n { name: { $ilike: `%${escapedSearch}%` } },\n { slug: { $ilike: `%${escapedSearch}%` } },\n ]\n }\n\n const offset = (page - 1) * pageSize\n const [paged, total] = await em.findAndCount(CustomerRole, where as any, {\n orderBy: { createdAt: 'ASC' },\n limit: pageSize,\n offset,\n })\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n const items = paged.map((role) => ({\n id: role.id,\n name: role.name,\n slug: role.slug,\n description: role.description || null,\n isDefault: role.isDefault,\n isSystem: role.isSystem,\n customerAssignable: role.customerAssignable,\n createdAt: role.createdAt,\n updatedAt: role.updatedAt || null,\n }))\n\n return NextResponse.json({ ok: true, items, total, totalPages, page })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.roles.manage'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = createRoleSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const existing = await em.findOne(CustomerRole, {\n tenantId: auth.tenantId,\n slug: parsed.data.slug,\n deletedAt: null,\n })\n if (existing) {\n return NextResponse.json({ ok: false, error: 'A role with this slug already exists' }, { status: 409 })\n }\n\n const role = em.create(CustomerRole, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n name: parsed.data.name,\n slug: parsed.data.slug,\n description: parsed.data.description || null,\n isDefault: parsed.data.isDefault ?? false,\n customerAssignable: parsed.data.customerAssignable ?? false,\n isSystem: false,\n createdAt: new Date(),\n } as any) as CustomerRole\n em.persist(role)\n\n const acl = em.create(CustomerRoleAcl, {\n role,\n tenantId: auth.tenantId,\n featuresJson: [],\n isPortalAdmin: false,\n createdAt: new Date(),\n } as any)\n em.persist(acl)\n\n await em.flush()\n\n void emitCustomerAccountsEvent('customer_accounts.role.created', {\n id: role.id,\n name: role.name,\n slug: role.slug,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n role: {\n id: role.id,\n name: role.name,\n slug: role.slug,\n description: role.description || null,\n isDefault: role.isDefault,\n isSystem: role.isSystem,\n customerAssignable: role.customerAssignable,\n createdAt: role.createdAt,\n },\n }, { status: 201 })\n}\n\nconst roleSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n slug: z.string(),\n description: z.string().nullable(),\n isDefault: z.boolean(),\n isSystem: z.boolean(),\n customerAssignable: z.boolean(),\n createdAt: z.string().datetime(),\n updatedAt: z.string().datetime().nullable(),\n})\n\nconst roleResponseSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n slug: z.string(),\n description: z.string().nullable(),\n isDefault: z.boolean(),\n isSystem: z.boolean(),\n customerAssignable: z.boolean(),\n createdAt: z.string().datetime(),\n})\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst getMethodDoc: OpenApiMethodDoc = {\n summary: 'List customer roles (admin)',\n description: 'Returns customer roles for the caller\\'s active organization, or for the organization passed as organizationId when it is within the caller\\'s organization scope.',\n tags: ['Customer Accounts Admin'],\n query: z.object({\n page: z.number().int().positive().optional(),\n pageSize: z.number().int().positive().max(100).optional(),\n search: z.string().optional(),\n organizationId: z.string().uuid().optional(),\n }),\n responses: [{\n status: 200,\n description: 'Role list',\n schema: z.object({ ok: z.literal(true), items: z.array(roleSchema), total: z.number(), totalPages: z.number(), page: z.number() }),\n }],\n errors: [\n { status: 400, description: 'Organization not found or outside the caller scope', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n ],\n}\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Create customer role (admin)',\n description: 'Creates a new customer role with an empty ACL.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: createRoleSchema },\n responses: [{ status: 201, description: 'Role created', schema: z.object({ ok: z.literal(true), role: roleResponseSchema }) }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 409, description: 'Slug already exists', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer role management (admin)',\n methods: {\n GET: getMethodDoc,\n POST: postMethodDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
5
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
@@ -9,6 +10,7 @@ import { adminCreateUserSchema } from "@open-mercato/core/modules/customer_accou
|
|
|
9
10
|
import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
|
|
10
11
|
import { findAndCountWithDecryption, findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
11
12
|
import { isOwnedCompanyEntity } from "@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership";
|
|
13
|
+
import { resolveAdminTargetOrganization } from "@open-mercato/core/modules/customer_accounts/lib/adminOrganizationScope";
|
|
12
14
|
import { lookupHashCandidates } from "@open-mercato/shared/lib/encryption/aes";
|
|
13
15
|
import { E } from "../../../../generated/entities.ids.generated.js";
|
|
14
16
|
import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
|
|
@@ -214,6 +216,21 @@ async function POST(req) {
|
|
|
214
216
|
if (!parsed.success) {
|
|
215
217
|
return NextResponse.json({ ok: false, error: "Validation failed", details: parsed.error.flatten().fieldErrors }, { status: 400 });
|
|
216
218
|
}
|
|
219
|
+
const target = await resolveAdminTargetOrganization({
|
|
220
|
+
container,
|
|
221
|
+
auth,
|
|
222
|
+
request: req,
|
|
223
|
+
requestedOrganizationId: parsed.data.organizationId
|
|
224
|
+
});
|
|
225
|
+
if (!target.ok) return target.response;
|
|
226
|
+
const organizationId = target.organizationId;
|
|
227
|
+
if (organizationId !== auth.orgId) {
|
|
228
|
+
const hasTargetAccess = await rbacService.userHasAllFeatures(auth.sub, ["customer_accounts.manage"], { tenantId: auth.tenantId, organizationId });
|
|
229
|
+
if (!hasTargetAccess) {
|
|
230
|
+
const { translate } = await resolveTranslations();
|
|
231
|
+
return NextResponse.json({ ok: false, error: translate("customer_accounts.admin.errors.insufficientPermissions", "Insufficient permissions") }, { status: 403 });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
217
234
|
const em = container.resolve("em");
|
|
218
235
|
const customerUserService = container.resolve("customerUserService");
|
|
219
236
|
const existing = await customerUserService.findByEmail(parsed.data.email, auth.tenantId);
|
|
@@ -229,11 +246,11 @@ async function POST(req) {
|
|
|
229
246
|
{
|
|
230
247
|
id: { $in: requestedRoleIds },
|
|
231
248
|
tenantId: auth.tenantId,
|
|
232
|
-
organizationId
|
|
249
|
+
organizationId,
|
|
233
250
|
deletedAt: null
|
|
234
251
|
},
|
|
235
252
|
void 0,
|
|
236
|
-
{ tenantId: auth.tenantId, organizationId
|
|
253
|
+
{ tenantId: auth.tenantId, organizationId }
|
|
237
254
|
);
|
|
238
255
|
if (validRoles.length !== requestedRoleIds.length) {
|
|
239
256
|
const foundIds = new Set(validRoles.map((role) => role.id));
|
|
@@ -245,7 +262,7 @@ async function POST(req) {
|
|
|
245
262
|
if (parsed.data.customerEntityId) {
|
|
246
263
|
const owned = await isOwnedCompanyEntity(em, parsed.data.customerEntityId, {
|
|
247
264
|
tenantId: auth.tenantId,
|
|
248
|
-
organizationId
|
|
265
|
+
organizationId
|
|
249
266
|
});
|
|
250
267
|
if (!owned) {
|
|
251
268
|
return NextResponse.json({ ok: false, error: "Company not found" }, { status: 400 });
|
|
@@ -255,7 +272,7 @@ async function POST(req) {
|
|
|
255
272
|
parsed.data.email,
|
|
256
273
|
parsed.data.password,
|
|
257
274
|
parsed.data.displayName,
|
|
258
|
-
{ tenantId: auth.tenantId, organizationId
|
|
275
|
+
{ tenantId: auth.tenantId, organizationId }
|
|
259
276
|
);
|
|
260
277
|
user.emailVerifiedAt = /* @__PURE__ */ new Date();
|
|
261
278
|
await em.transactional(async (tx) => {
|
|
@@ -277,7 +294,7 @@ async function POST(req) {
|
|
|
277
294
|
id: user.id,
|
|
278
295
|
email: user.email,
|
|
279
296
|
tenantId: auth.tenantId,
|
|
280
|
-
organizationId
|
|
297
|
+
organizationId,
|
|
281
298
|
createdBy: auth.sub
|
|
282
299
|
}).catch(() => void 0);
|
|
283
300
|
return NextResponse.json({
|
|
@@ -349,12 +366,12 @@ const getMethodDoc = {
|
|
|
349
366
|
};
|
|
350
367
|
const postMethodDoc = {
|
|
351
368
|
summary: "Create customer user (admin)",
|
|
352
|
-
description: "Creates a new customer user directly. Staff-initiated, bypasses signup flow.",
|
|
369
|
+
description: "Creates a new customer user directly. Staff-initiated, bypasses signup flow. Pass organizationId to create the user in a specific organization within the caller's organization scope; it defaults to the caller's active organization. Roles and the company link are validated against the target organization.",
|
|
353
370
|
tags: ["Customer Accounts Admin"],
|
|
354
371
|
requestBody: { schema: adminCreateUserSchema },
|
|
355
372
|
responses: [{ status: 201, description: "User created", schema: successSchema }],
|
|
356
373
|
errors: [
|
|
357
|
-
{ status: 400, description: "Validation failed", schema: errorSchema },
|
|
374
|
+
{ status: 400, description: "Validation failed, organization not found or outside the caller scope, or no organization could be resolved", schema: errorSchema },
|
|
358
375
|
{ status: 401, description: "Not authenticated", schema: errorSchema },
|
|
359
376
|
{ status: 403, description: "Insufficient permissions", schema: errorSchema },
|
|
360
377
|
{ status: 409, description: "Email already exists", schema: errorSchema }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/admin/users.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } 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 { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerUser, CustomerUserRole, CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { adminCreateUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { findAndCountWithDecryption, findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOwnedCompanyEntity } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'\nimport { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { E } from '#generated/entities.ids.generated'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { sql } from 'kysely'\n\nconst EMAIL_LIKE_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nexport const metadata = {}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.view'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n const em = container.resolve('em') as EntityManager\n\n const url = new URL(req.url)\n const page = Math.max(1, parseInt(url.searchParams.get('page') || '1'))\n const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get('pageSize') || '25')))\n const status = url.searchParams.get('status') as 'active' | 'inactive' | 'locked' | null\n const customerEntityId = url.searchParams.get('customerEntityId')\n const personEntityId = url.searchParams.get('personEntityId')\n const roleId = url.searchParams.get('roleId')\n const search = url.searchParams.get('search')\n\n const organizationScope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const where: Record<string, unknown> = {\n tenantId: auth.tenantId,\n deletedAt: null,\n }\n if (Array.isArray(organizationScope.filterIds)) {\n if (organizationScope.filterIds.length === 0) {\n return NextResponse.json({\n ok: true,\n items: [],\n total: 0,\n totalPages: 1,\n page,\n })\n }\n where.organizationId = { $in: organizationScope.filterIds }\n }\n\n if (status === 'active') {\n where.isActive = true\n where.$or = [{ lockedUntil: null }, { lockedUntil: { $lt: new Date() } }]\n } else if (status === 'inactive') {\n where.isActive = false\n } else if (status === 'locked') {\n where.lockedUntil = { $gt: new Date() }\n }\n\n if (customerEntityId) {\n where.customerEntityId = customerEntityId\n }\n\n if (personEntityId) {\n where.personEntityId = personEntityId\n }\n\n if (search) {\n const trimmedSearch = search.trim()\n // email/displayName are stored encrypted, so SQL ILIKE on the ciphertext\n // never matches a plaintext search term. Use search_tokens table for partial\n // matches and emailHash for exact email lookups.\n const searchFilter: Record<string, unknown>[] = []\n\n // Search encrypted fields via search_tokens\n const matchedIds = await findCustomerUserIdsBySearchTokens(em, E.customer_accounts.customer_user, trimmedSearch, auth.tenantId)\n if (matchedIds && matchedIds.length > 0) {\n searchFilter.push({ id: { $in: matchedIds } })\n }\n\n // Also support exact email lookup via emailHash\n if (EMAIL_LIKE_PATTERN.test(search)) {\n searchFilter.push({ emailHash: { $in: lookupHashCandidates(search) } })\n }\n\n if (searchFilter.length > 0) {\n if (where.$or) {\n where.$and = [{ $or: where.$or }, { $or: searchFilter }]\n delete where.$or\n } else {\n where.$or = searchFilter\n }\n } else {\n // No search results found, return empty\n return NextResponse.json({\n ok: true,\n items: [],\n total: 0,\n totalPages: 1,\n page,\n })\n }\n }\n\n let userIds: string[] | null = null\n if (roleId) {\n // Validate the roleId against the scoped CustomerRole set before touching the\n // junction table. CustomerUserRole carries no tenant/org column of its own,\n // so an unscoped lookup here is a role-UUID existence oracle and is brittle\n // against future code that reads the link rows directly (#2693, defence-in-depth).\n const scopedRole = await findOneWithDecryption(\n em,\n CustomerRole,\n { id: roleId, tenantId: auth.tenantId, organizationId: auth.orgId, deletedAt: null } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!scopedRole) {\n return NextResponse.json({\n ok: true,\n items: [],\n total: 0,\n totalPages: 1,\n page,\n })\n }\n const roleLinks = await findWithDecryption(\n em,\n CustomerUserRole,\n { role: roleId as any, deletedAt: null } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n userIds = roleLinks.map((link) => (link.user as any)?.id || (link.user as unknown as string))\n if (userIds.length === 0) {\n return NextResponse.json({\n ok: true,\n items: [],\n total: 0,\n totalPages: 1,\n page,\n })\n }\n where.id = { $in: userIds }\n }\n\n const offset = (page - 1) * pageSize\n const [users, total] = await findAndCountWithDecryption(\n em,\n CustomerUser,\n where as any,\n {\n orderBy: { createdAt: 'DESC' },\n limit: pageSize,\n offset,\n },\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n\n const pageUserIds = users.map((user) => user.id)\n const userRoleLinks = pageUserIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerUserRole,\n { user: { $in: pageUserIds } as any, deletedAt: null } as any,\n { populate: ['role'] },\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n : []\n\n const rolesByUserId = new Map<string, Array<{ id: string; name: string; slug: string }>>()\n for (const link of userRoleLinks) {\n const linkUserId = (link.user as any)?.id ?? (link.user as unknown as string)\n const role = link.role as any\n const bucket = rolesByUserId.get(linkUserId)\n const entry = { id: role.id, name: role.name, slug: role.slug }\n if (bucket) bucket.push(entry)\n else rolesByUserId.set(linkUserId, [entry])\n }\n\n const pageOrganizationIds = Array.from(new Set(\n users.map((user) => user.organizationId).filter((organizationId): organizationId is string => !!organizationId),\n ))\n const organizations = pageOrganizationIds.length > 0\n ? await em.find(Organization, { id: { $in: pageOrganizationIds }, tenant: auth.tenantId, deletedAt: null })\n : []\n const organizationNameById = new Map(organizations.map((organization) => [organization.id, organization.name]))\n\n const items = users.map((user) => {\n const organizationId = user.organizationId ?? null\n return {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n isActive: user.isActive,\n lockedUntil: user.lockedUntil || null,\n lastLoginAt: user.lastLoginAt || null,\n customerEntityId: user.customerEntityId || null,\n personEntityId: user.personEntityId || null,\n organizationId,\n organizationName: organizationId ? organizationNameById.get(organizationId) ?? null : null,\n createdAt: user.createdAt,\n updatedAt: user.updatedAt || null,\n roles: rolesByUserId.get(user.id) ?? [],\n }\n })\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n return NextResponse.json({\n ok: true,\n items,\n total,\n totalPages,\n page,\n })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.manage'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = adminCreateUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as EntityManager\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n\n const existing = await customerUserService.findByEmail(parsed.data.email, auth.tenantId!)\n if (existing) {\n return NextResponse.json({ ok: false, error: 'A user with this email already exists' }, { status: 409 })\n }\n\n // Resolve roles up front, scoped to the caller's tenant AND organization, and\n // reject the request if any requested role is missing from the scoped set.\n // CustomerRole is org-scoped, so omitting organizationId here would let an\n // admin in org A link roles from org B in the same tenant \u2014 a cross-org\n // privilege grant (#2693). Invalid IDs must be rejected, not silently dropped.\n let resolvedRoles: Array<{ id: string }> = []\n if (parsed.data.roleIds && parsed.data.roleIds.length > 0) {\n const requestedRoleIds = parsed.data.roleIds\n const validRoles = await findWithDecryption(\n em,\n CustomerRole,\n {\n id: { $in: requestedRoleIds } as any,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (validRoles.length !== requestedRoleIds.length) {\n const foundIds = new Set(validRoles.map((role) => role.id))\n const missingId = requestedRoleIds.find((roleId) => !foundIds.has(roleId))\n return NextResponse.json({ ok: false, error: `Role ${missingId} not found` }, { status: 400 })\n }\n resolvedRoles = validRoles\n }\n\n // Reject a customerEntityId the caller does not own. Without this check a\n // mislinked company FK persists indefinitely and cross-links the user into\n // another org/company's portal context (#2693).\n if (parsed.data.customerEntityId) {\n const owned = await isOwnedCompanyEntity(em, parsed.data.customerEntityId, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!owned) {\n return NextResponse.json({ ok: false, error: 'Company not found' }, { status: 400 })\n }\n }\n\n const user = await customerUserService.createUser(\n parsed.data.email,\n parsed.data.password,\n parsed.data.displayName,\n { tenantId: auth.tenantId!, organizationId: auth.orgId! },\n )\n user.emailVerifiedAt = new Date()\n\n // Persist the user, its company association, and its role links in one\n // transaction so a flush failure on the role loop cannot leave a roleless\n // user committed (privilege gap).\n await em.transactional(async (tx) => {\n tx.persist(user)\n await tx.flush()\n\n if (parsed.data.customerEntityId) {\n await tx.nativeUpdate(CustomerUser, { id: user.id }, { customerEntityId: parsed.data.customerEntityId })\n }\n\n for (const role of resolvedRoles) {\n const userRole = tx.create(CustomerUserRole, {\n user,\n role,\n createdAt: new Date(),\n } as any)\n tx.persist(userRole)\n }\n })\n\n void emitCustomerAccountsEvent('customer_accounts.user.created', {\n id: user.id,\n email: user.email,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n createdBy: auth.sub,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n user: { id: user.id, email: user.email, displayName: user.displayName },\n }, { status: 201 })\n}\n\nconst roleSchema = z.object({ id: z.string().uuid(), name: z.string(), slug: z.string() })\nconst userSchema = z.object({\n id: z.string().uuid(),\n email: z.string(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n isActive: z.boolean(),\n lockedUntil: z.string().datetime().nullable(),\n lastLoginAt: z.string().datetime().nullable(),\n customerEntityId: z.string().uuid().nullable(),\n personEntityId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable().describe('Organization the customer user belongs to'),\n organizationName: z.string().nullable().describe('Resolved organization display name'),\n createdAt: z.string().datetime(),\n updatedAt: z.string().datetime().nullable(),\n roles: z.array(roleSchema),\n})\n\nconst successSchema = z.object({\n ok: z.literal(true),\n user: z.object({ id: z.string().uuid(), email: z.string(), displayName: z.string() }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nasync function findCustomerUserIdsBySearchTokens(\n em: EntityManager,\n entityType: string,\n search: string,\n tenantScope: string | null | undefined,\n field?: string,\n): Promise<string[] | null> {\n const trimmed = search.trim()\n if (!trimmed) return null\n const searchConfig = resolveSearchConfig()\n if (!searchConfig.enabled) return []\n const { hashes } = tokenizeText(trimmed, searchConfig)\n if (!hashes.length) return []\n\n const db = (em as any).getKysely() as any\n let query = db\n .selectFrom('search_tokens')\n .select('entity_id')\n .where('entity_type', '=', entityType)\n .where('token_hash', 'in', hashes)\n .groupBy('entity_id')\n .having(sql<boolean>`count(distinct token_hash) >= ${hashes.length}`)\n if (field) {\n query = query.where('field', '=', field)\n }\n if (tenantScope !== undefined) {\n query = query.where(sql<boolean>`tenant_id is not distinct from ${tenantScope}`)\n }\n const rows = (await query.execute()) as Array<{ entity_id?: unknown }>\n return rows\n .map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nconst getMethodDoc: OpenApiMethodDoc = {\n summary: 'List customer users (admin)',\n description: 'Returns a paginated list of customer users with roles. Supports filtering by status, company, role, and search.',\n tags: ['Customer Accounts Admin'],\n query: z.object({\n page: z.number().int().positive().optional(),\n pageSize: z.number().int().positive().max(100).optional(),\n status: z.enum(['active', 'inactive', 'locked']).optional(),\n customerEntityId: z.string().uuid().optional(),\n roleId: z.string().uuid().optional(),\n search: z.string().optional(),\n }),\n responses: [{\n status: 200,\n description: 'Paginated user list',\n schema: z.object({ ok: z.literal(true), items: z.array(userSchema), total: z.number(), totalPages: z.number(), page: z.number() }),\n }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n ],\n}\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Create customer user (admin)',\n description: 'Creates a new customer user directly. Staff-initiated, bypasses signup flow.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: adminCreateUserSchema },\n responses: [{ status: 201, description: 'User created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 409, description: 'Email already exists', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer user management (admin)',\n methods: {\n GET: getMethodDoc,\n POST: postMethodDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } 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 { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerUser, CustomerUserRole, CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { adminCreateUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { findAndCountWithDecryption, findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOwnedCompanyEntity } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'\nimport { resolveAdminTargetOrganization } from '@open-mercato/core/modules/customer_accounts/lib/adminOrganizationScope'\nimport { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { E } from '#generated/entities.ids.generated'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { sql } from 'kysely'\n\nconst EMAIL_LIKE_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nexport const metadata = {}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.view'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n const em = container.resolve('em') as EntityManager\n\n const url = new URL(req.url)\n const page = Math.max(1, parseInt(url.searchParams.get('page') || '1'))\n const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get('pageSize') || '25')))\n const status = url.searchParams.get('status') as 'active' | 'inactive' | 'locked' | null\n const customerEntityId = url.searchParams.get('customerEntityId')\n const personEntityId = url.searchParams.get('personEntityId')\n const roleId = url.searchParams.get('roleId')\n const search = url.searchParams.get('search')\n\n const organizationScope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const where: Record<string, unknown> = {\n tenantId: auth.tenantId,\n deletedAt: null,\n }\n if (Array.isArray(organizationScope.filterIds)) {\n if (organizationScope.filterIds.length === 0) {\n return NextResponse.json({\n ok: true,\n items: [],\n total: 0,\n totalPages: 1,\n page,\n })\n }\n where.organizationId = { $in: organizationScope.filterIds }\n }\n\n if (status === 'active') {\n where.isActive = true\n where.$or = [{ lockedUntil: null }, { lockedUntil: { $lt: new Date() } }]\n } else if (status === 'inactive') {\n where.isActive = false\n } else if (status === 'locked') {\n where.lockedUntil = { $gt: new Date() }\n }\n\n if (customerEntityId) {\n where.customerEntityId = customerEntityId\n }\n\n if (personEntityId) {\n where.personEntityId = personEntityId\n }\n\n if (search) {\n const trimmedSearch = search.trim()\n // email/displayName are stored encrypted, so SQL ILIKE on the ciphertext\n // never matches a plaintext search term. Use search_tokens table for partial\n // matches and emailHash for exact email lookups.\n const searchFilter: Record<string, unknown>[] = []\n\n // Search encrypted fields via search_tokens\n const matchedIds = await findCustomerUserIdsBySearchTokens(em, E.customer_accounts.customer_user, trimmedSearch, auth.tenantId)\n if (matchedIds && matchedIds.length > 0) {\n searchFilter.push({ id: { $in: matchedIds } })\n }\n\n // Also support exact email lookup via emailHash\n if (EMAIL_LIKE_PATTERN.test(search)) {\n searchFilter.push({ emailHash: { $in: lookupHashCandidates(search) } })\n }\n\n if (searchFilter.length > 0) {\n if (where.$or) {\n where.$and = [{ $or: where.$or }, { $or: searchFilter }]\n delete where.$or\n } else {\n where.$or = searchFilter\n }\n } else {\n // No search results found, return empty\n return NextResponse.json({\n ok: true,\n items: [],\n total: 0,\n totalPages: 1,\n page,\n })\n }\n }\n\n let userIds: string[] | null = null\n if (roleId) {\n // Validate the roleId against the scoped CustomerRole set before touching the\n // junction table. CustomerUserRole carries no tenant/org column of its own,\n // so an unscoped lookup here is a role-UUID existence oracle and is brittle\n // against future code that reads the link rows directly (#2693, defence-in-depth).\n const scopedRole = await findOneWithDecryption(\n em,\n CustomerRole,\n { id: roleId, tenantId: auth.tenantId, organizationId: auth.orgId, deletedAt: null } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!scopedRole) {\n return NextResponse.json({\n ok: true,\n items: [],\n total: 0,\n totalPages: 1,\n page,\n })\n }\n const roleLinks = await findWithDecryption(\n em,\n CustomerUserRole,\n { role: roleId as any, deletedAt: null } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n userIds = roleLinks.map((link) => (link.user as any)?.id || (link.user as unknown as string))\n if (userIds.length === 0) {\n return NextResponse.json({\n ok: true,\n items: [],\n total: 0,\n totalPages: 1,\n page,\n })\n }\n where.id = { $in: userIds }\n }\n\n const offset = (page - 1) * pageSize\n const [users, total] = await findAndCountWithDecryption(\n em,\n CustomerUser,\n where as any,\n {\n orderBy: { createdAt: 'DESC' },\n limit: pageSize,\n offset,\n },\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n\n const pageUserIds = users.map((user) => user.id)\n const userRoleLinks = pageUserIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerUserRole,\n { user: { $in: pageUserIds } as any, deletedAt: null } as any,\n { populate: ['role'] },\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n : []\n\n const rolesByUserId = new Map<string, Array<{ id: string; name: string; slug: string }>>()\n for (const link of userRoleLinks) {\n const linkUserId = (link.user as any)?.id ?? (link.user as unknown as string)\n const role = link.role as any\n const bucket = rolesByUserId.get(linkUserId)\n const entry = { id: role.id, name: role.name, slug: role.slug }\n if (bucket) bucket.push(entry)\n else rolesByUserId.set(linkUserId, [entry])\n }\n\n const pageOrganizationIds = Array.from(new Set(\n users.map((user) => user.organizationId).filter((organizationId): organizationId is string => !!organizationId),\n ))\n const organizations = pageOrganizationIds.length > 0\n ? await em.find(Organization, { id: { $in: pageOrganizationIds }, tenant: auth.tenantId, deletedAt: null })\n : []\n const organizationNameById = new Map(organizations.map((organization) => [organization.id, organization.name]))\n\n const items = users.map((user) => {\n const organizationId = user.organizationId ?? null\n return {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n isActive: user.isActive,\n lockedUntil: user.lockedUntil || null,\n lastLoginAt: user.lastLoginAt || null,\n customerEntityId: user.customerEntityId || null,\n personEntityId: user.personEntityId || null,\n organizationId,\n organizationName: organizationId ? organizationNameById.get(organizationId) ?? null : null,\n createdAt: user.createdAt,\n updatedAt: user.updatedAt || null,\n roles: rolesByUserId.get(user.id) ?? [],\n }\n })\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n return NextResponse.json({\n ok: true,\n items,\n total,\n totalPages,\n page,\n })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.manage'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = adminCreateUserSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const target = await resolveAdminTargetOrganization({\n container,\n auth,\n request: req,\n requestedOrganizationId: parsed.data.organizationId,\n })\n if (!target.ok) return target.response\n const organizationId = target.organizationId\n\n if (organizationId !== auth.orgId) {\n const hasTargetAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.manage'], { tenantId: auth.tenantId, organizationId })\n if (!hasTargetAccess) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({ ok: false, error: translate('customer_accounts.admin.errors.insufficientPermissions', 'Insufficient permissions') }, { status: 403 })\n }\n }\n\n const em = container.resolve('em') as EntityManager\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n\n const existing = await customerUserService.findByEmail(parsed.data.email, auth.tenantId!)\n if (existing) {\n return NextResponse.json({ ok: false, error: 'A user with this email already exists' }, { status: 409 })\n }\n\n // Resolve roles up front, scoped to the caller's tenant AND organization, and\n // reject the request if any requested role is missing from the scoped set.\n // CustomerRole is org-scoped, so omitting organizationId here would let an\n // admin in org A link roles from org B in the same tenant \u2014 a cross-org\n // privilege grant (#2693). Invalid IDs must be rejected, not silently dropped.\n let resolvedRoles: Array<{ id: string }> = []\n if (parsed.data.roleIds && parsed.data.roleIds.length > 0) {\n const requestedRoleIds = parsed.data.roleIds\n const validRoles = await findWithDecryption(\n em,\n CustomerRole,\n {\n id: { $in: requestedRoleIds } as any,\n tenantId: auth.tenantId,\n organizationId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId: auth.tenantId, organizationId },\n )\n if (validRoles.length !== requestedRoleIds.length) {\n const foundIds = new Set(validRoles.map((role) => role.id))\n const missingId = requestedRoleIds.find((roleId) => !foundIds.has(roleId))\n return NextResponse.json({ ok: false, error: `Role ${missingId} not found` }, { status: 400 })\n }\n resolvedRoles = validRoles\n }\n\n // Reject a customerEntityId the caller does not own. Without this check a\n // mislinked company FK persists indefinitely and cross-links the user into\n // another org/company's portal context (#2693).\n if (parsed.data.customerEntityId) {\n const owned = await isOwnedCompanyEntity(em, parsed.data.customerEntityId, {\n tenantId: auth.tenantId,\n organizationId,\n })\n if (!owned) {\n return NextResponse.json({ ok: false, error: 'Company not found' }, { status: 400 })\n }\n }\n\n const user = await customerUserService.createUser(\n parsed.data.email,\n parsed.data.password,\n parsed.data.displayName,\n { tenantId: auth.tenantId!, organizationId },\n )\n user.emailVerifiedAt = new Date()\n\n // Persist the user, its company association, and its role links in one\n // transaction so a flush failure on the role loop cannot leave a roleless\n // user committed (privilege gap).\n await em.transactional(async (tx) => {\n tx.persist(user)\n await tx.flush()\n\n if (parsed.data.customerEntityId) {\n await tx.nativeUpdate(CustomerUser, { id: user.id }, { customerEntityId: parsed.data.customerEntityId })\n }\n\n for (const role of resolvedRoles) {\n const userRole = tx.create(CustomerUserRole, {\n user,\n role,\n createdAt: new Date(),\n } as any)\n tx.persist(userRole)\n }\n })\n\n void emitCustomerAccountsEvent('customer_accounts.user.created', {\n id: user.id,\n email: user.email,\n tenantId: auth.tenantId,\n organizationId,\n createdBy: auth.sub,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n user: { id: user.id, email: user.email, displayName: user.displayName },\n }, { status: 201 })\n}\n\nconst roleSchema = z.object({ id: z.string().uuid(), name: z.string(), slug: z.string() })\nconst userSchema = z.object({\n id: z.string().uuid(),\n email: z.string(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n isActive: z.boolean(),\n lockedUntil: z.string().datetime().nullable(),\n lastLoginAt: z.string().datetime().nullable(),\n customerEntityId: z.string().uuid().nullable(),\n personEntityId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable().describe('Organization the customer user belongs to'),\n organizationName: z.string().nullable().describe('Resolved organization display name'),\n createdAt: z.string().datetime(),\n updatedAt: z.string().datetime().nullable(),\n roles: z.array(roleSchema),\n})\n\nconst successSchema = z.object({\n ok: z.literal(true),\n user: z.object({ id: z.string().uuid(), email: z.string(), displayName: z.string() }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nasync function findCustomerUserIdsBySearchTokens(\n em: EntityManager,\n entityType: string,\n search: string,\n tenantScope: string | null | undefined,\n field?: string,\n): Promise<string[] | null> {\n const trimmed = search.trim()\n if (!trimmed) return null\n const searchConfig = resolveSearchConfig()\n if (!searchConfig.enabled) return []\n const { hashes } = tokenizeText(trimmed, searchConfig)\n if (!hashes.length) return []\n\n const db = (em as any).getKysely() as any\n let query = db\n .selectFrom('search_tokens')\n .select('entity_id')\n .where('entity_type', '=', entityType)\n .where('token_hash', 'in', hashes)\n .groupBy('entity_id')\n .having(sql<boolean>`count(distinct token_hash) >= ${hashes.length}`)\n if (field) {\n query = query.where('field', '=', field)\n }\n if (tenantScope !== undefined) {\n query = query.where(sql<boolean>`tenant_id is not distinct from ${tenantScope}`)\n }\n const rows = (await query.execute()) as Array<{ entity_id?: unknown }>\n return rows\n .map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nconst getMethodDoc: OpenApiMethodDoc = {\n summary: 'List customer users (admin)',\n description: 'Returns a paginated list of customer users with roles. Supports filtering by status, company, role, and search.',\n tags: ['Customer Accounts Admin'],\n query: z.object({\n page: z.number().int().positive().optional(),\n pageSize: z.number().int().positive().max(100).optional(),\n status: z.enum(['active', 'inactive', 'locked']).optional(),\n customerEntityId: z.string().uuid().optional(),\n roleId: z.string().uuid().optional(),\n search: z.string().optional(),\n }),\n responses: [{\n status: 200,\n description: 'Paginated user list',\n schema: z.object({ ok: z.literal(true), items: z.array(userSchema), total: z.number(), totalPages: z.number(), page: z.number() }),\n }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n ],\n}\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Create customer user (admin)',\n description: 'Creates a new customer user directly. Staff-initiated, bypasses signup flow. Pass organizationId to create the user in a specific organization within the caller\\'s organization scope; it defaults to the caller\\'s active organization. Roles and the company link are validated against the target organization.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: adminCreateUserSchema },\n responses: [{ status: 201, description: 'User created', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed, organization not found or outside the caller scope, or no organization could be resolved', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 409, description: 'Email already exists', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer user management (admin)',\n methods: {\n GET: getMethodDoc,\n POST: postMethodDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js
CHANGED
|
@@ -6,6 +6,7 @@ import Link from "next/link";
|
|
|
6
6
|
import { useRouter } from "next/navigation";
|
|
7
7
|
import { Globe, Settings } from "lucide-react";
|
|
8
8
|
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
9
|
+
import { ErrorMessage, LoadingMessage } from "@open-mercato/ui/backend/detail";
|
|
9
10
|
import { RowActions } from "@open-mercato/ui/backend/RowActions";
|
|
10
11
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
11
12
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
@@ -19,6 +20,7 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
|
19
20
|
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
20
21
|
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
21
22
|
import { ListEmptyState } from "@open-mercato/ui/backend/filters/ListEmptyState";
|
|
23
|
+
import { OrganizationSelect } from "@open-mercato/core/modules/directory/components/OrganizationSelect";
|
|
22
24
|
import { buildPortalRootUrl, buildPortalUrlPattern } from "../../../lib/portalUrl.js";
|
|
23
25
|
import { useDemoPortalAccounts } from "../useDemoPortalAccounts.js";
|
|
24
26
|
function formatDate(value, fallback) {
|
|
@@ -27,35 +29,64 @@ function formatDate(value, fallback) {
|
|
|
27
29
|
if (Number.isNaN(date.getTime())) return fallback;
|
|
28
30
|
return date.toLocaleDateString();
|
|
29
31
|
}
|
|
30
|
-
async function fetchRoleFilterOptions() {
|
|
32
|
+
async function fetchRoleFilterOptions(organizationId) {
|
|
31
33
|
try {
|
|
34
|
+
const params = new URLSearchParams({ pageSize: "100" });
|
|
35
|
+
if (organizationId) params.set("organizationId", organizationId);
|
|
32
36
|
const call = await apiCall(
|
|
33
|
-
|
|
37
|
+
`/api/customer_accounts/admin/roles?${params.toString()}`
|
|
34
38
|
);
|
|
35
|
-
if (!call.ok) return
|
|
36
|
-
const items =
|
|
39
|
+
if (!call.ok || !Array.isArray(call.result?.items)) return null;
|
|
40
|
+
const items = call.result.items;
|
|
37
41
|
return items.filter((item) => typeof item?.id === "string" && typeof item?.name === "string").map((item) => ({ value: item.id, label: item.name, id: item.id }));
|
|
38
42
|
} catch {
|
|
39
|
-
return
|
|
43
|
+
return null;
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
function CreateUserDialog({
|
|
43
47
|
open,
|
|
44
48
|
onOpenChange,
|
|
45
|
-
roleOptions,
|
|
46
49
|
onCreated,
|
|
47
|
-
onRunMutation
|
|
50
|
+
onRunMutation,
|
|
51
|
+
activeOrganizationId
|
|
48
52
|
}) {
|
|
49
53
|
const t = useT();
|
|
50
54
|
const [email, setEmail] = React.useState("");
|
|
51
55
|
const [displayName, setDisplayName] = React.useState("");
|
|
52
56
|
const [password, setPassword] = React.useState("");
|
|
57
|
+
const [organizationId, setOrganizationId] = React.useState(null);
|
|
58
|
+
const [organizationRoles, setOrganizationRoles] = React.useState(null);
|
|
59
|
+
const [roleReloadToken, setRoleReloadToken] = React.useState(0);
|
|
53
60
|
const [selectedRoleIds, setSelectedRoleIds] = React.useState([]);
|
|
54
61
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
55
62
|
const resetForm = React.useCallback(() => {
|
|
56
63
|
setEmail("");
|
|
57
64
|
setDisplayName("");
|
|
58
65
|
setPassword("");
|
|
66
|
+
setOrganizationId(null);
|
|
67
|
+
setSelectedRoleIds([]);
|
|
68
|
+
}, []);
|
|
69
|
+
const requireOrganization = !activeOrganizationId;
|
|
70
|
+
const roleOrganizationId = organizationId ?? activeOrganizationId;
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (!open || !roleOrganizationId) {
|
|
73
|
+
setOrganizationRoles(null);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
setOrganizationRoles({ organizationId: roleOrganizationId, status: "loading" });
|
|
78
|
+
fetchRoleFilterOptions(roleOrganizationId).then((opts) => {
|
|
79
|
+
if (cancelled) return;
|
|
80
|
+
setOrganizationRoles(opts === null ? { organizationId: roleOrganizationId, status: "error" } : { organizationId: roleOrganizationId, status: "loaded", options: opts });
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
cancelled = true;
|
|
84
|
+
};
|
|
85
|
+
}, [open, roleOrganizationId, roleReloadToken]);
|
|
86
|
+
const currentOrganizationRoles = organizationRoles?.organizationId === roleOrganizationId ? organizationRoles : null;
|
|
87
|
+
const availableRoleOptions = currentOrganizationRoles?.status === "loaded" ? currentOrganizationRoles.options : [];
|
|
88
|
+
const handleOrganizationChange = React.useCallback((next) => {
|
|
89
|
+
setOrganizationId(next ?? null);
|
|
59
90
|
setSelectedRoleIds([]);
|
|
60
91
|
}, []);
|
|
61
92
|
const handleSubmit = React.useCallback(async (event) => {
|
|
@@ -64,6 +95,10 @@ function CreateUserDialog({
|
|
|
64
95
|
flash(t("customer_accounts.admin.createUser.error.required", "Email, name, and password are required"), "error");
|
|
65
96
|
return;
|
|
66
97
|
}
|
|
98
|
+
if (requireOrganization && !organizationId) {
|
|
99
|
+
flash(t("customer_accounts.admin.createUser.error.organizationRequired", "Select an organization for the new user"), "error");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
67
102
|
setIsSubmitting(true);
|
|
68
103
|
try {
|
|
69
104
|
await onRunMutation(async () => {
|
|
@@ -76,7 +111,8 @@ function CreateUserDialog({
|
|
|
76
111
|
email: email.trim(),
|
|
77
112
|
displayName: displayName.trim(),
|
|
78
113
|
password,
|
|
79
|
-
roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : void 0
|
|
114
|
+
roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : void 0,
|
|
115
|
+
organizationId: organizationId ?? void 0
|
|
80
116
|
})
|
|
81
117
|
}
|
|
82
118
|
);
|
|
@@ -95,7 +131,7 @@ function CreateUserDialog({
|
|
|
95
131
|
} finally {
|
|
96
132
|
setIsSubmitting(false);
|
|
97
133
|
}
|
|
98
|
-
}, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t]);
|
|
134
|
+
}, [displayName, email, onCreated, onOpenChange, onRunMutation, organizationId, password, requireOrganization, resetForm, selectedRoleIds, t]);
|
|
99
135
|
const handleKeyDown = React.useCallback((event) => {
|
|
100
136
|
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
101
137
|
event.preventDefault();
|
|
@@ -153,9 +189,31 @@ function CreateUserDialog({
|
|
|
153
189
|
}
|
|
154
190
|
)
|
|
155
191
|
] }),
|
|
156
|
-
|
|
192
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
193
|
+
/* @__PURE__ */ jsx("label", { className: "text-sm font-medium", htmlFor: "create-organization", children: t("customer_accounts.admin.createUser.fields.organization", "Organization") }),
|
|
194
|
+
/* @__PURE__ */ jsx(
|
|
195
|
+
OrganizationSelect,
|
|
196
|
+
{
|
|
197
|
+
id: "create-organization",
|
|
198
|
+
value: organizationId,
|
|
199
|
+
onChange: handleOrganizationChange,
|
|
200
|
+
required: requireOrganization,
|
|
201
|
+
includeEmptyOption: true,
|
|
202
|
+
emptyOptionLabel: requireOrganization ? t("customer_accounts.admin.createUser.fields.organizationPlaceholder", "Select an organization") : t("customer_accounts.admin.createUser.fields.organizationCurrent", "Current organization"),
|
|
203
|
+
className: "w-full h-9 rounded border px-2 text-sm"
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
] }),
|
|
207
|
+
roleOrganizationId && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
157
208
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("customer_accounts.admin.createUser.fields.roles", "Roles") }),
|
|
158
|
-
|
|
209
|
+
currentOrganizationRoles?.status === "error" ? /* @__PURE__ */ jsx(
|
|
210
|
+
ErrorMessage,
|
|
211
|
+
{
|
|
212
|
+
label: t("customer_accounts.admin.createUser.error.loadRoles", "Could not load roles for this organization"),
|
|
213
|
+
action: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setRoleReloadToken((token) => token + 1), children: t("customer_accounts.admin.createUser.actions.retryRoles", "Retry") })
|
|
214
|
+
}
|
|
215
|
+
) : currentOrganizationRoles?.status !== "loaded" ? /* @__PURE__ */ jsx(LoadingMessage, { label: t("customer_accounts.admin.createUser.loadingRoles", "Loading roles...") }) : availableRoleOptions.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("customer_accounts.admin.createUser.noRoles", "No roles are available for this organization") }) : null,
|
|
216
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: availableRoleOptions.map((role) => {
|
|
159
217
|
const isSelected = selectedRoleIds.includes(role.id);
|
|
160
218
|
return /* @__PURE__ */ jsx(
|
|
161
219
|
Button,
|
|
@@ -184,7 +242,11 @@ function CreateUserDialog({
|
|
|
184
242
|
] })
|
|
185
243
|
] }) });
|
|
186
244
|
}
|
|
187
|
-
function PortalUsersPageClient({
|
|
245
|
+
function PortalUsersPageClient({
|
|
246
|
+
portalOrigin,
|
|
247
|
+
portalOrgSlug = null,
|
|
248
|
+
activeOrganizationId = null
|
|
249
|
+
}) {
|
|
188
250
|
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
189
251
|
const t = useT();
|
|
190
252
|
const router = useRouter();
|
|
@@ -217,7 +279,7 @@ function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }) {
|
|
|
217
279
|
React.useEffect(() => {
|
|
218
280
|
let cancelled = false;
|
|
219
281
|
fetchRoleFilterOptions().then((opts) => {
|
|
220
|
-
if (!cancelled) setRoleOptions(opts);
|
|
282
|
+
if (!cancelled) setRoleOptions(opts ?? []);
|
|
221
283
|
});
|
|
222
284
|
return () => {
|
|
223
285
|
cancelled = true;
|
|
@@ -526,9 +588,9 @@ function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }) {
|
|
|
526
588
|
{
|
|
527
589
|
open: createDialogOpen,
|
|
528
590
|
onOpenChange: setCreateDialogOpen,
|
|
529
|
-
roleOptions,
|
|
530
591
|
onCreated: () => setReloadToken((token) => token + 1),
|
|
531
|
-
onRunMutation: runMutationWithContext
|
|
592
|
+
onRunMutation: runMutationWithContext,
|
|
593
|
+
activeOrganizationId
|
|
532
594
|
}
|
|
533
595
|
),
|
|
534
596
|
ConfirmDialogElement
|