@open-mercato/core 0.10.1-develop.7833.1.d6cbf0e3ab → 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/cart/api/current/lines/bulk/route.js +1 -1
- package/dist/modules/cart/api/current/lines/bulk/route.js.map +2 -2
- package/dist/modules/cart/api/current/lines/route.js +1 -1
- package/dist/modules/cart/api/current/lines/route.js.map +2 -2
- package/dist/modules/cart/commands/lines.js +5 -10
- package/dist/modules/cart/commands/lines.js.map +2 -2
- package/dist/modules/cart/lib/currentCartRoute.js +8 -0
- package/dist/modules/cart/lib/currentCartRoute.js.map +3 -3
- package/dist/modules/cart/lib/lineMutationLock.js +52 -0
- package/dist/modules/cart/lib/lineMutationLock.js.map +7 -0
- 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/sales/commands/documents.js +3 -3
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +75 -21
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.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/cart/api/current/lines/bulk/route.ts +1 -1
- package/src/modules/cart/api/current/lines/route.ts +1 -1
- package/src/modules/cart/commands/lines.ts +5 -10
- package/src/modules/cart/i18n/de.json +1 -0
- package/src/modules/cart/i18n/en.json +1 -0
- package/src/modules/cart/i18n/es.json +1 -0
- package/src/modules/cart/i18n/ko.json +1 -0
- package/src/modules/cart/i18n/pl.json +1 -0
- package/src/modules/cart/lib/currentCartRoute.ts +8 -0
- package/src/modules/cart/lib/lineMutationLock.ts +59 -0
- 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/sales/commands/documents.ts +3 -3
- package/src/modules/sales/components/documents/ItemsSection.tsx +82 -23
- package/src/modules/sales/components/documents/lineItemUtils.ts +2 -2
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/ko.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/cart/lib/currentCartRoute.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nimport { getCartCommand } from '../commands/cart'\nimport type { CartGetByTokenInput, CartSummary } from '../commands/cart'\nimport type { CartPriceChange } from './reprice'\nimport { isCartStoreContextUnavailableError, resolveCartRouteContext } from './routeContext'\nimport type { ResolveCartRouteContextOptions, ResolvedCartRouteContext } from './routeContext'\n\n// Step 4.7 \u2014 shared plumbing for the four `/api/cart/current/*` route files\n// (`current/route.ts`, `current/lines/route.ts`, `current/lines/[lineId]/route.ts`,\n// `current/lines/bulk/route.ts`). Kept here rather than duplicated per file because\n// every one of them needs the identical: resolve-the-store-context-at-the-edge\n// step (\u00A76a.1a), the `X-Cart-Token` header read (\u00A78.3/\u00A710, never a path segment or\n// query string), the token\u2192cart lookup via `cart.get`, and the same CrudHttpError /\n// interceptor-rejection \u2192 HTTP mapping.\n\n/**\n * \u00A78.1's literal resourceKind for every cart mutation's optimistic-lock AND\n * mutation-guard call \u2014 the lock helper's own `resourceKind` input, paired with\n * `resourceId: cart.id`, `current: cart.updatedAt` and `request: ctx.request`. Used\n * for line-level mutations too \u2014 a `CartLine` has its own `updated_at`, but the lock\n * token this module's concurrency contract tracks is the CART aggregate's, per\n * \u00A78.1's own text (which names `cart.cart` for every mutating command, not a\n * per-sub-resource kind).\n */\nexport const CART_RESOURCE_KIND = 'cart.cart'\n\n/** \u00A78.3/\u00A710: the ONLY place a cart token travels \u2014 never a path segment, never a query string. */\nexport const CART_TOKEN_HEADER = 'X-Cart-Token'\n\nexport function readCartToken(request: Request): string | null {\n const value = request.headers.get(CART_TOKEN_HEADER)\n const trimmed = value?.trim()\n return trimmed ? trimmed : null\n}\n\nexport type ResolvedContextResult =\n | { ok: true; routeContext: ResolvedCartRouteContext }\n | { ok: false; response: NextResponse }\n\n/**\n * Wraps `resolveCartRouteContext` (Step 4.3) and maps\n * `CartStoreContextUnavailableError` to a `503` \u2014 per that error's own doc comment\n * (\"the route layer must map this to a 503\") and this step's explicit brief.\n *\n * (Step 7.3) Accepts an optional `opts`, forwarded verbatim to\n * `resolveCartRouteContext` \u2014 D-R4-22's \"lock and merge routes resolve the buyer context\n * fresh (`{ fresh: true }`)\". Every pre-existing caller omits it and is unaffected\n * (`undefined` forwards to the same default `resolveCartRouteContext` already had).\n */\nexport async function resolveRouteContextOrError(\n request: Request,\n container: AppContainer,\n opts?: ResolveCartRouteContextOptions,\n): Promise<ResolvedContextResult> {\n try {\n const routeContext = await resolveCartRouteContext(request, container, opts)\n return { ok: true, routeContext }\n } catch (error) {\n if (isCartStoreContextUnavailableError(error)) {\n const { translate } = await resolveTranslations()\n return {\n ok: false,\n response: NextResponse.json(\n {\n error: translate(\n 'cart.errors.store_context_unavailable',\n 'Cart is unavailable for this store right now',\n ),\n code: 'cart_store_context_unavailable',\n },\n { status: 503 },\n ),\n }\n }\n throw error\n }\n}\n\n/**\n * Builds the `CommandRuntimeContext` every `/current/*` command call shares.\n * `auth` stays `null` \u2014 the storefront caller is anonymous, or a buyer with no\n * staff identity at all (`ResolvedCartRouteContext.customerUserId` is where an\n * identified buyer's identity already lives); this mirrors `cart.create`'s own\n * documented \"Pattern C\" assumption (`commands/cart.ts`). Tenant/organization scope\n * for the commands' own `ensureTenantScope`/`ensureOrganizationScope` defense-in-depth\n * checks comes from `selectedOrganizationId`, which the resolved store context\n * already pins \u2014 not from an (absent) staff `auth` scope.\n */\nexport function buildCommandContext(\n container: AppContainer,\n request: Request,\n routeContext: ResolvedCartRouteContext,\n): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: routeContext.organizationId,\n organizationIds: null,\n request,\n }\n}\n\n/**\n * (Step 5.1) `resolveCurrentCart`'s result. `priceChanges` is always an array (possibly\n * empty); `versionBumpReason` is set exactly when \u00A75.2 triggers 2-4 fired and `cart.get`\n * ran `cart.reprice` internally for this call \u2014 see `commands/cart.ts`'s `getCartCommand`.\n */\nexport type ResolvedCurrentCart = {\n cart: CartSummary\n priceChanges: CartPriceChange[]\n versionBumpReason?: 'repricing'\n}\n\n/**\n * Resolves the \"current\" cart addressed by the presented `X-Cart-Token`, scoped to\n * the resolved store's tenant/organization (\u00A78.3: `kind = 'basket'` only, via\n * `cart.get`). Returns `null` when the token does not resolve \u2014 a wrong token, a\n * cross-tenant token and a real `kind = 'proposal'` cart all collapse to the same\n * `null` here, matching `cart.get`'s own no-enumeration-oracle contract.\n *\n * (Step 5.1) Always supplies `freshContext: routeContext` \u2014 every one of the four\n * `/current/*` routes that calls this helper has already resolved a `StoreContext` at the\n * route edge, so `cart.get` always has a fresh buyer context to compare against, not only\n * on the literal `GET /current` route: \"per-request BuyerContext digest comparison (also\n * on GET)\" (PLAN.md Step 5.1) is implemented once, here, for every caller.\n */\nexport async function resolveCurrentCart(\n commandBus: CommandBus,\n ctx: CommandRuntimeContext,\n routeContext: ResolvedCartRouteContext,\n token: string,\n): Promise<ResolvedCurrentCart | null> {\n const input: CartGetByTokenInput = {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n token,\n freshContext: routeContext,\n }\n const { result } = await commandBus.execute<CartGetByTokenInput, Awaited<ReturnType<typeof getCartCommand.execute>>>(\n getCartCommand.id,\n { input, ctx },\n )\n if (!result.found) return null\n return {\n cart: result.cart,\n priceChanges: result.priceChanges ?? [],\n versionBumpReason: result.versionBumpReason,\n }\n}\n\nexport function notFoundCartResponse(): NextResponse {\n return NextResponse.json({ error: '[internal] cart not found' }, { status: 404 })\n}\n\nconst logger = createLogger('cart').child({ component: 'api' })\n\n/**\n * Standard catch-block mapping for every `/current/*` route: a thrown\n * `CrudHttpError` (badRequest/notFound/conflict from a command, or the standardized\n * optimistic-lock/idempotency-mismatch 409/422) echoes its own status/body; a\n * command-interceptor rejection (none exist for `cart` yet, but the mapping is the\n * same one `promotions/api/[id]/tree/route.ts` uses for an identical custom\n * command-backed route) echoes its own status/body; anything else is an\n * unexpected 500, logged and reported.\n */\nexport async function mapRouteErrorToResponse(error: unknown): Promise<NextResponse> {\n if (isCrudHttpError(error)) return NextResponse.json(error.body, { status: error.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n logger.error('cart api route failed', { err: error })\n getTelemetryRuntime()?.reportError(error, { module: 'cart', code: 'cart.api_route_failed' })\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('cart.errors.unexpected', 'Unexpected error') }, { status: 500 })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAG7B,SAAS,0CAA0C;AACnD,SAAS,uBAAuB;AAChC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAEpC,SAAS,sBAAsB;AAG/B,SAAS,oCAAoC,+BAA+B;AAoBrE,MAAM,qBAAqB;AAG3B,MAAM,oBAAoB;AAE1B,SAAS,cAAc,SAAiC;AAC7D,QAAM,QAAQ,QAAQ,QAAQ,IAAI,iBAAiB;AACnD,QAAM,UAAU,OAAO,KAAK;AAC5B,SAAO,UAAU,UAAU;AAC7B;AAgBA,eAAsB,2BACpB,SACA,WACA,MACgC;AAChC,MAAI;AACF,UAAM,eAAe,MAAM,wBAAwB,SAAS,WAAW,IAAI;AAC3E,WAAO,EAAE,IAAI,MAAM,aAAa;AAAA,EAClC,SAAS,OAAO;AACd,QAAI,mCAAmC,KAAK,GAAG;AAC7C,YAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,UAAU,aAAa;AAAA,UACrB;AAAA,YACE,OAAO;AAAA,cACL;AAAA,cACA;AAAA,YACF;AAAA,YACA,MAAM;AAAA,UACR;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAYO,SAAS,oBACd,WACA,SACA,cACuB;AACvB,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,mBAAmB;AAAA,IACnB,wBAAwB,aAAa;AAAA,IACrC,iBAAiB;AAAA,IACjB;AAAA,EACF;AACF;AA0BA,eAAsB,mBACpB,YACA,KACA,cACA,OACqC;AACrC,QAAM,QAA6B;AAAA,IACjC,UAAU,aAAa;AAAA,IACvB,gBAAgB,aAAa;AAAA,IAC7B;AAAA,IACA,cAAc;AAAA,EAChB;AACA,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW;AAAA,IAClC,eAAe;AAAA,IACf,EAAE,OAAO,IAAI;AAAA,EACf;AACA,MAAI,CAAC,OAAO,MAAO,QAAO;AAC1B,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,cAAc,OAAO,gBAAgB,CAAC;AAAA,IACtC,mBAAmB,OAAO;AAAA,EAC5B;AACF;AAEO,SAAS,uBAAqC;AACnD,SAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAClF;AAEA,MAAM,SAAS,aAAa,MAAM,EAAE,MAAM,EAAE,WAAW,MAAM,CAAC;AAW9D,eAAsB,wBAAwB,OAAuC;AACnF,MAAI,gBAAgB,KAAK,EAAG,QAAO,aAAa,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,OAAO,CAAC;AACzF,QAAM,uBAAuB,mCAAmC,KAAK;AACrE,MAAI,qBAAsB,QAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AACrH,SAAO,MAAM,yBAAyB,EAAE,KAAK,MAAM,CAAC;AACpD,sBAAoB,GAAG,YAAY,OAAO,EAAE,QAAQ,QAAQ,MAAM,wBAAwB,CAAC;AAC3F,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,SAAO,aAAa,KAAK,EAAE,OAAO,UAAU,0BAA0B,kBAAkB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9G;",
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nimport { getCartCommand } from '../commands/cart'\nimport type { CartGetByTokenInput, CartSummary } from '../commands/cart'\nimport type { CartPriceChange } from './reprice'\nimport { isCartLineMutationLockTimeoutError } from './lineMutationLock'\nimport { isCartStoreContextUnavailableError, resolveCartRouteContext } from './routeContext'\nimport type { ResolveCartRouteContextOptions, ResolvedCartRouteContext } from './routeContext'\n\n// Step 4.7 \u2014 shared plumbing for the four `/api/cart/current/*` route files\n// (`current/route.ts`, `current/lines/route.ts`, `current/lines/[lineId]/route.ts`,\n// `current/lines/bulk/route.ts`). Kept here rather than duplicated per file because\n// every one of them needs the identical: resolve-the-store-context-at-the-edge\n// step (\u00A76a.1a), the `X-Cart-Token` header read (\u00A78.3/\u00A710, never a path segment or\n// query string), the token\u2192cart lookup via `cart.get`, and the same CrudHttpError /\n// interceptor-rejection \u2192 HTTP mapping.\n\n/**\n * \u00A78.1's literal resourceKind for every cart mutation's optimistic-lock AND\n * mutation-guard call \u2014 the lock helper's own `resourceKind` input, paired with\n * `resourceId: cart.id`, `current: cart.updatedAt` and `request: ctx.request`. Used\n * for line-level mutations too \u2014 a `CartLine` has its own `updated_at`, but the lock\n * token this module's concurrency contract tracks is the CART aggregate's, per\n * \u00A78.1's own text (which names `cart.cart` for every mutating command, not a\n * per-sub-resource kind).\n */\nexport const CART_RESOURCE_KIND = 'cart.cart'\n\n/** \u00A78.3/\u00A710: the ONLY place a cart token travels \u2014 never a path segment, never a query string. */\nexport const CART_TOKEN_HEADER = 'X-Cart-Token'\n\nexport function readCartToken(request: Request): string | null {\n const value = request.headers.get(CART_TOKEN_HEADER)\n const trimmed = value?.trim()\n return trimmed ? trimmed : null\n}\n\nexport type ResolvedContextResult =\n | { ok: true; routeContext: ResolvedCartRouteContext }\n | { ok: false; response: NextResponse }\n\n/**\n * Wraps `resolveCartRouteContext` (Step 4.3) and maps\n * `CartStoreContextUnavailableError` to a `503` \u2014 per that error's own doc comment\n * (\"the route layer must map this to a 503\") and this step's explicit brief.\n *\n * (Step 7.3) Accepts an optional `opts`, forwarded verbatim to\n * `resolveCartRouteContext` \u2014 D-R4-22's \"lock and merge routes resolve the buyer context\n * fresh (`{ fresh: true }`)\". Every pre-existing caller omits it and is unaffected\n * (`undefined` forwards to the same default `resolveCartRouteContext` already had).\n */\nexport async function resolveRouteContextOrError(\n request: Request,\n container: AppContainer,\n opts?: ResolveCartRouteContextOptions,\n): Promise<ResolvedContextResult> {\n try {\n const routeContext = await resolveCartRouteContext(request, container, opts)\n return { ok: true, routeContext }\n } catch (error) {\n if (isCartStoreContextUnavailableError(error)) {\n const { translate } = await resolveTranslations()\n return {\n ok: false,\n response: NextResponse.json(\n {\n error: translate(\n 'cart.errors.store_context_unavailable',\n 'Cart is unavailable for this store right now',\n ),\n code: 'cart_store_context_unavailable',\n },\n { status: 503 },\n ),\n }\n }\n throw error\n }\n}\n\n/**\n * Builds the `CommandRuntimeContext` every `/current/*` command call shares.\n * `auth` stays `null` \u2014 the storefront caller is anonymous, or a buyer with no\n * staff identity at all (`ResolvedCartRouteContext.customerUserId` is where an\n * identified buyer's identity already lives); this mirrors `cart.create`'s own\n * documented \"Pattern C\" assumption (`commands/cart.ts`). Tenant/organization scope\n * for the commands' own `ensureTenantScope`/`ensureOrganizationScope` defense-in-depth\n * checks comes from `selectedOrganizationId`, which the resolved store context\n * already pins \u2014 not from an (absent) staff `auth` scope.\n */\nexport function buildCommandContext(\n container: AppContainer,\n request: Request,\n routeContext: ResolvedCartRouteContext,\n): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: routeContext.organizationId,\n organizationIds: null,\n request,\n }\n}\n\n/**\n * (Step 5.1) `resolveCurrentCart`'s result. `priceChanges` is always an array (possibly\n * empty); `versionBumpReason` is set exactly when \u00A75.2 triggers 2-4 fired and `cart.get`\n * ran `cart.reprice` internally for this call \u2014 see `commands/cart.ts`'s `getCartCommand`.\n */\nexport type ResolvedCurrentCart = {\n cart: CartSummary\n priceChanges: CartPriceChange[]\n versionBumpReason?: 'repricing'\n}\n\n/**\n * Resolves the \"current\" cart addressed by the presented `X-Cart-Token`, scoped to\n * the resolved store's tenant/organization (\u00A78.3: `kind = 'basket'` only, via\n * `cart.get`). Returns `null` when the token does not resolve \u2014 a wrong token, a\n * cross-tenant token and a real `kind = 'proposal'` cart all collapse to the same\n * `null` here, matching `cart.get`'s own no-enumeration-oracle contract.\n *\n * (Step 5.1) Always supplies `freshContext: routeContext` \u2014 every one of the four\n * `/current/*` routes that calls this helper has already resolved a `StoreContext` at the\n * route edge, so `cart.get` always has a fresh buyer context to compare against, not only\n * on the literal `GET /current` route: \"per-request BuyerContext digest comparison (also\n * on GET)\" (PLAN.md Step 5.1) is implemented once, here, for every caller.\n */\nexport async function resolveCurrentCart(\n commandBus: CommandBus,\n ctx: CommandRuntimeContext,\n routeContext: ResolvedCartRouteContext,\n token: string,\n): Promise<ResolvedCurrentCart | null> {\n const input: CartGetByTokenInput = {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n token,\n freshContext: routeContext,\n }\n const { result } = await commandBus.execute<CartGetByTokenInput, Awaited<ReturnType<typeof getCartCommand.execute>>>(\n getCartCommand.id,\n { input, ctx },\n )\n if (!result.found) return null\n return {\n cart: result.cart,\n priceChanges: result.priceChanges ?? [],\n versionBumpReason: result.versionBumpReason,\n }\n}\n\nexport function notFoundCartResponse(): NextResponse {\n return NextResponse.json({ error: '[internal] cart not found' }, { status: 404 })\n}\n\nconst logger = createLogger('cart').child({ component: 'api' })\n\n/**\n * Standard catch-block mapping for every `/current/*` route: a thrown\n * `CrudHttpError` (badRequest/notFound/conflict from a command, or the standardized\n * optimistic-lock/idempotency-mismatch 409/422) echoes its own status/body; a\n * command-interceptor rejection (none exist for `cart` yet, but the mapping is the\n * same one `promotions/api/[id]/tree/route.ts` uses for an identical custom\n * command-backed route) echoes its own status/body; anything else is an\n * unexpected 500, logged and reported.\n */\nexport async function mapRouteErrorToResponse(error: unknown): Promise<NextResponse> {\n if (isCartLineMutationLockTimeoutError(error)) {\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n { error: translate('cart.errors.busy', 'Cart is busy. Please try again.'), code: 'cart_busy' },\n { status: 503, headers: { 'Retry-After': '1', 'Cache-Control': 'no-store' } },\n )\n }\n if (isCrudHttpError(error)) return NextResponse.json(error.body, { status: error.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n logger.error('cart api route failed', { err: error })\n getTelemetryRuntime()?.reportError(error, { module: 'cart', code: 'cart.api_route_failed' })\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('cart.errors.unexpected', 'Unexpected error') }, { status: 500 })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["translate"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
var _a, _b;
|
|
2
|
+
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
3
|
+
import { calculateBackoffDelayMs } from "@open-mercato/shared/lib/delivery/retry";
|
|
4
|
+
const CART_LINE_LOCK_TIMEOUT = /* @__PURE__ */ Symbol.for("@open-mercato/cart:LineMutationLockTimeout");
|
|
5
|
+
class CartLineMutationLockTimeoutError extends (_b = Error, _a = CART_LINE_LOCK_TIMEOUT, _b) {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("[internal] Cart line mutation lock wait deadline exceeded");
|
|
8
|
+
this[_a] = true;
|
|
9
|
+
this.name = "CartLineMutationLockTimeoutError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function isCartLineMutationLockTimeoutError(error) {
|
|
13
|
+
return typeof error === "object" && error !== null && Reflect.get(error, CART_LINE_LOCK_TIMEOUT) === true;
|
|
14
|
+
}
|
|
15
|
+
async function withCartLineMutationLock(em, cartId, phases, label, waitDeadlineMs = 15e3) {
|
|
16
|
+
const key = `cart:lines:${cartId}`;
|
|
17
|
+
const deadline = Date.now() + waitDeadlineMs;
|
|
18
|
+
const busy = new Error("[internal] Cart line mutation lock is busy");
|
|
19
|
+
const joinsTransaction = em.isInTransaction();
|
|
20
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
21
|
+
try {
|
|
22
|
+
await withAtomicFlush(em, [
|
|
23
|
+
async () => {
|
|
24
|
+
if (joinsTransaction) {
|
|
25
|
+
await em.execute("select pg_advisory_xact_lock(hashtextextended(?::text, 0))", [key]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const rows = await em.execute(
|
|
29
|
+
"select pg_try_advisory_xact_lock(hashtextextended(?::text, 0)) as locked",
|
|
30
|
+
[key]
|
|
31
|
+
);
|
|
32
|
+
if (!rows[0]?.locked) throw busy;
|
|
33
|
+
},
|
|
34
|
+
...phases
|
|
35
|
+
], { transaction: true, label });
|
|
36
|
+
return;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error !== busy || em.isInTransaction()) throw error;
|
|
39
|
+
}
|
|
40
|
+
const remainingMs = deadline - Date.now();
|
|
41
|
+
if (remainingMs <= 0) throw new CartLineMutationLockTimeoutError();
|
|
42
|
+
const delayMs = calculateBackoffDelayMs(Math.min(attempt, 5), { baseDelayMs: 50, maxJitterMs: 25 });
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingMs)));
|
|
44
|
+
if (Date.now() >= deadline) throw new CartLineMutationLockTimeoutError();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
CartLineMutationLockTimeoutError,
|
|
49
|
+
isCartLineMutationLockTimeoutError,
|
|
50
|
+
withCartLineMutationLock
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=lineMutationLock.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cart/lib/lineMutationLock.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { calculateBackoffDelayMs } from '@open-mercato/shared/lib/delivery/retry'\n\nconst CART_LINE_LOCK_TIMEOUT = Symbol.for('@open-mercato/cart:LineMutationLockTimeout')\n\nexport class CartLineMutationLockTimeoutError extends Error {\n readonly [CART_LINE_LOCK_TIMEOUT] = true\n\n constructor() {\n super('[internal] Cart line mutation lock wait deadline exceeded')\n this.name = 'CartLineMutationLockTimeoutError'\n }\n}\n\nexport function isCartLineMutationLockTimeoutError(error: unknown): error is CartLineMutationLockTimeoutError {\n return typeof error === 'object' && error !== null && Reflect.get(error, CART_LINE_LOCK_TIMEOUT) === true\n}\n\nexport async function withCartLineMutationLock(\n em: EntityManager,\n cartId: string,\n phases: Array<() => void | Promise<void>>,\n label: string,\n waitDeadlineMs = 15_000,\n): Promise<void> {\n const key = `cart:lines:${cartId}`\n const deadline = Date.now() + waitDeadlineMs\n const busy = new Error('[internal] Cart line mutation lock is busy')\n const joinsTransaction = em.isInTransaction()\n\n for (let attempt = 1; ; attempt += 1) {\n try {\n await withAtomicFlush(em, [\n async () => {\n if (joinsTransaction) {\n await em.execute('select pg_advisory_xact_lock(hashtextextended(?::text, 0))', [key])\n return\n }\n const rows = await em.execute<Array<{ locked: boolean }>>(\n 'select pg_try_advisory_xact_lock(hashtextextended(?::text, 0)) as locked',\n [key],\n )\n if (!rows[0]?.locked) throw busy\n },\n ...phases,\n ], { transaction: true, label })\n return\n } catch (error) {\n if (error !== busy || em.isInTransaction()) throw error\n }\n\n const remainingMs = deadline - Date.now()\n if (remainingMs <= 0) throw new CartLineMutationLockTimeoutError()\n const delayMs = calculateBackoffDelayMs(Math.min(attempt, 5), { baseDelayMs: 50, maxJitterMs: 25 })\n await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingMs)))\n if (Date.now() >= deadline) throw new CartLineMutationLockTimeoutError()\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AACA,SAAS,uBAAuB;AAChC,SAAS,+BAA+B;AAExC,MAAM,yBAAyB,uBAAO,IAAI,4CAA4C;AAE/E,MAAM,0CAAyC,YAC1C,6BAD0C,IAAM;AAAA,EAG1D,cAAc;AACZ,UAAM,2DAA2D;AAHnE,SAAU,MAA0B;AAIlC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,mCAAmC,OAA2D;AAC5G,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,QAAQ,IAAI,OAAO,sBAAsB,MAAM;AACvG;AAEA,eAAsB,yBACpB,IACA,QACA,QACA,OACA,iBAAiB,MACF;AACf,QAAM,MAAM,cAAc,MAAM;AAChC,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,QAAM,OAAO,IAAI,MAAM,4CAA4C;AACnE,QAAM,mBAAmB,GAAG,gBAAgB;AAE5C,WAAS,UAAU,KAAK,WAAW,GAAG;AACpC,QAAI;AACF,YAAM,gBAAgB,IAAI;AAAA,QACxB,YAAY;AACV,cAAI,kBAAkB;AACpB,kBAAM,GAAG,QAAQ,8DAA8D,CAAC,GAAG,CAAC;AACpF;AAAA,UACF;AACA,gBAAM,OAAO,MAAM,GAAG;AAAA,YACpB;AAAA,YACA,CAAC,GAAG;AAAA,UACN;AACA,cAAI,CAAC,KAAK,CAAC,GAAG,OAAQ,OAAM;AAAA,QAC9B;AAAA,QACA,GAAG;AAAA,MACL,GAAG,EAAE,aAAa,MAAM,MAAM,CAAC;AAC/B;AAAA,IACF,SAAS,OAAO;AACd,UAAI,UAAU,QAAQ,GAAG,gBAAgB,EAAG,OAAM;AAAA,IACpD;AAEA,UAAM,cAAc,WAAW,KAAK,IAAI;AACxC,QAAI,eAAe,EAAG,OAAM,IAAI,iCAAiC;AACjE,UAAM,UAAU,wBAAwB,KAAK,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,IAAI,aAAa,GAAG,CAAC;AAClG,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,IAAI,SAAS,WAAW,CAAC,CAAC;AAClF,QAAI,KAAK,IAAI,KAAK,SAAU,OAAM,IAAI,iCAAiC;AAAA,EACzE;AACF;",
|
|
6
|
+
"names": []
|
|
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";
|
|
@@ -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
|
}
|