@open-mercato/core 0.10.1-develop.7679.1.cb2c13f49e → 0.10.1-develop.7687.1.e1a6f9de51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/users/route.js +1 -1
  3. package/dist/modules/auth/api/users/route.js.map +2 -2
  4. package/dist/modules/customer_accounts/api/magic-link/request.js +15 -1
  5. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  6. package/dist/modules/customer_accounts/api/password/reset-request.js +15 -1
  7. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  8. package/dist/modules/customer_accounts/emails/CustomerAuthLinkEmail.js +26 -0
  9. package/dist/modules/customer_accounts/emails/CustomerAuthLinkEmail.js.map +7 -0
  10. package/dist/modules/customer_accounts/lib/authLinkEmails.js +67 -0
  11. package/dist/modules/customer_accounts/lib/authLinkEmails.js.map +7 -0
  12. package/dist/modules/messages/commands/actions.js +8 -1
  13. package/dist/modules/messages/commands/actions.js.map +2 -2
  14. package/dist/modules/payment_gateways/data/entities.js +4 -0
  15. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  16. package/dist/modules/payment_gateways/lib/session-idempotency.js +29 -5
  17. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  18. package/dist/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.js +19 -0
  19. package/dist/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.js.map +7 -0
  20. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +3 -3
  21. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +2 -2
  22. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.js +59 -0
  23. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.js.map +7 -0
  24. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.js +11 -0
  25. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.js.map +7 -0
  26. package/dist/modules/query_index/lib/engine.js +60 -13
  27. package/dist/modules/query_index/lib/engine.js.map +2 -2
  28. package/package.json +7 -7
  29. package/src/modules/auth/api/users/route.ts +1 -1
  30. package/src/modules/customer_accounts/api/magic-link/request.ts +16 -3
  31. package/src/modules/customer_accounts/api/password/reset-request.ts +16 -3
  32. package/src/modules/customer_accounts/emails/CustomerAuthLinkEmail.tsx +45 -0
  33. package/src/modules/customer_accounts/i18n/de.json +12 -0
  34. package/src/modules/customer_accounts/i18n/en.json +12 -0
  35. package/src/modules/customer_accounts/i18n/es.json +12 -0
  36. package/src/modules/customer_accounts/i18n/ko.json +12 -0
  37. package/src/modules/customer_accounts/i18n/pl.json +12 -0
  38. package/src/modules/customer_accounts/lib/authLinkEmails.ts +77 -0
  39. package/src/modules/messages/commands/actions.ts +8 -1
  40. package/src/modules/payment_gateways/data/entities.ts +4 -0
  41. package/src/modules/payment_gateways/lib/session-idempotency.ts +41 -5
  42. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +14 -1
  43. package/src/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.ts +18 -0
  44. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +3 -3
  45. package/src/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.ts +9 -0
  46. package/src/modules/portal/frontend/[orgSlug]/portal/magic-link/page.tsx +71 -0
  47. package/src/modules/portal/i18n/de.json +7 -0
  48. package/src/modules/portal/i18n/en.json +7 -0
  49. package/src/modules/portal/i18n/es.json +7 -0
  50. package/src/modules/portal/i18n/ko.json +7 -0
  51. package/src/modules/portal/i18n/pl.json +7 -0
  52. package/src/modules/query_index/lib/engine.ts +70 -12
@@ -1,4 +1,4 @@
1
- [build:core] found 5996 entry points
1
+ [build:core] found 6003 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 266 entry points
4
4
  [build:core:generated] built successfully
@@ -354,7 +354,7 @@ async function GET(req) {
354
354
  { user: { $in: userIds }, deletedAt: null },
355
355
  { populate: ["role"] },
356
356
  {
357
- tenantId: effectiveTenantId ?? auth.tenantId ?? null,
357
+ tenantId: effectiveTenantId,
358
358
  organizationId: scopeOrganizationId
359
359
  }
360
360
  ) : [];
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/auth/api/users/route.ts"],
4
- "sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { User, Role, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { userCrudEvents, userCrudIndexer } from '@open-mercato/core/modules/auth/commands/users'\nimport {\n assertActorCanAccessUserTarget,\n assertActorCanAssignUserDestination,\n assertActorCanGrantRoleTokens,\n assertActorCanModifySuperAdminUserTarget,\n listSuperAdminUserIds,\n resolveUserDestinationRoles,\n throwUserDestinationOrganizationNotFound,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanFlag } from '@open-mercato/shared/lib/boolean'\nimport { MAX_USER_LOOKUP_IDS, resolveUserIdFilter } from '@open-mercato/core/modules/auth/lib/userIdFilter'\nimport { findEntityIdsBySearchTokensCompat, type SearchTokenDatabase } from '@open-mercato/shared/lib/search/tokenLookup'\nimport { normalizeDisplayNameInput } from '@open-mercato/core/modules/auth/lib/displayName'\nimport {\n getSelectedTenantFromRequest,\n resolveOrganizationScopeForRequest,\n} from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'users' })\n\nconst querySchema = z.object({\n id: z.string().uuid().optional(),\n ids: z.string().optional().describe('Comma-separated user identifiers, at most 100'),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n name: z.string().optional(),\n organizationId: z.string().uuid().optional(),\n scopeToActiveOrganization: z.boolean().optional(),\n roleIds: z.array(z.string().uuid()).optional(),\n}).passthrough()\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst passwordSchema = buildPasswordSchema()\n\nconst displayNameSchema = z.preprocess(\n normalizeDisplayNameInput,\n z.string().trim().min(1).max(120).nullable().optional(),\n)\n\nconst userCreateSchema = z.object({\n email: z.string().email(),\n name: displayNameSchema,\n password: passwordSchema.optional(),\n sendInviteEmail: z.boolean().optional(),\n organizationId: z.string().uuid(),\n roles: z.array(z.string()).optional(),\n}).refine(\n (data) => data.password || data.sendInviteEmail,\n { message: 'Either password or sendInviteEmail is required', path: ['password'] },\n)\n\nconst userUpdateSchema = z.object({\n id: z.string().uuid(),\n email: z.string().email().optional(),\n name: displayNameSchema,\n password: passwordSchema.optional(),\n organizationId: z.string().uuid().optional(),\n roles: z.array(z.string()).optional(),\n isConfirmed: z.boolean().optional(),\n})\n\nconst userListItemSchema = z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n name: z.string().nullable(),\n organizationId: z.string().uuid().nullable(),\n organizationName: z.string().nullable(),\n tenantId: z.string().uuid().nullable(),\n tenantName: z.string().nullable(),\n roles: z.array(z.string()),\n roleIds: z.array(z.string().uuid()).optional(),\n hasPassword: z.boolean().optional(),\n isConfirmed: z.boolean(),\n updatedAt: z.string().nullable().optional(),\n})\n\nconst userListResponseSchema = z.object({\n items: z.array(userListItemSchema),\n total: z.number().int().nonnegative(),\n totalPages: z.number().int().positive(),\n isSuperAdmin: z.boolean().optional(),\n})\n\nconst okResponseSchema = z.object({ ok: z.literal(true) })\n\nconst errorResponseSchema = z.object({ error: z.string() })\n\ntype CrudInput = Record<string, unknown>\ntype UserListFilter = Record<string, unknown>\n\n// UserRole carries no tenant/organization columns of its own, so the caller's scope has to be\n// expressed as a predicate on the `user` relation \u2014 MikroORM compiles that into a database-side\n// join against `users` with the scope predicates in the WHERE clause, keeping the link lookup\n// bounded to the scope instead of every tenant holding the role.\nfunction buildRoleLinkFilter(\n roleIdList: string[],\n userScope: UserListFilter[],\n candidateUserIds: Set<string> | null,\n): UserListFilter {\n const scope = candidateUserIds\n ? [...userScope, { id: { $in: Array.from(candidateUserIds) } }]\n : userScope\n return {\n role: { $in: roleIdList },\n deletedAt: null,\n user: scope.length > 1 ? { $and: scope } : scope[0],\n }\n}\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.users.list'] },\n POST: { requireAuth: true, requireFeatures: ['auth.users.create'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.users.edit'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.users.delete'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: User,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: userCrudEvents,\n indexer: userCrudIndexer,\n actions: {\n create: {\n commandId: 'auth.users.create',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n if (ctx.request) {\n await assertCanAssignRoles(ctx.request, parsed.roles, parsed)\n }\n return parsed\n },\n response: ({ result }) => ({\n id: String(result.user.id),\n ...(result.warning ? { _warning: result.warning } : {}),\n }),\n status: 201,\n },\n update: {\n commandId: 'auth.users.update',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n if (ctx.request) {\n if (typeof parsed.id === 'string' && parsed.id.length) {\n await assertCanModifySuperAdminTarget(ctx.request, parsed.id)\n await assertCanAccessUserTarget(ctx.request, parsed.id)\n }\n if (typeof parsed.organizationId === 'string' && parsed.organizationId.length) {\n const destinationChanged = await assertCanAssignUserDestination(ctx.request, parsed)\n if (!destinationChanged) {\n await assertCanAssignRoles(ctx.request, parsed.roles, parsed)\n }\n } else {\n await assertCanAssignRoles(ctx.request, parsed.roles, parsed)\n }\n }\n return parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'auth.users.delete',\n mapInput: async ({ parsed, raw, ctx }) => {\n const targetId = resolveDeleteTargetId(parsed, raw)\n if (ctx.request && targetId) {\n await assertCanModifySuperAdminTarget(ctx.request, targetId)\n await assertCanAccessUserTarget(ctx.request, targetId)\n }\n return parsed\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const url = new URL(req.url)\n const rawRoleIds = url.searchParams.getAll('roleId').filter((id): id is string => typeof id === 'string' && id.trim().length > 0)\n // Accept both the repeated (`?ids=a&ids=b`) and comma-joined (`?ids=a,b`) spellings.\n const rawIds = url.searchParams.getAll('ids').join(',') || undefined\n const userIdFilter = resolveUserIdFilter(rawIds, url.searchParams.get('id'))\n const parsed = querySchema.safeParse({\n id: url.searchParams.get('id') || undefined,\n ids: rawIds,\n page: url.searchParams.get('page') || undefined,\n // A caller resolving a batch of ids wants all of them; without this the default page of 50\n // would silently truncate a 100-id lookup.\n pageSize: url.searchParams.get('pageSize')\n || (rawIds && userIdFilter.kind === 'ids' ? String(Math.min(userIdFilter.ids.length, MAX_USER_LOOKUP_IDS)) : undefined),\n search: url.searchParams.get('search') || undefined,\n name: url.searchParams.get('name') || undefined,\n organizationId: url.searchParams.get('organizationId') || undefined,\n scopeToActiveOrganization: parseBooleanFlag(url.searchParams.get('scopeToActiveOrganization') || undefined),\n roleIds: rawRoleIds.length ? rawRoleIds : undefined,\n })\n if (!parsed.success) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n let isSuperAdmin = auth.isSuperAdmin === true\n try {\n if (auth.sub) {\n const rbacService = container.resolve('rbacService') as any\n const acl = await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n isSuperAdmin = isSuperAdmin || !!acl?.isSuperAdmin\n }\n } catch (err) {\n logger.error('Failed to resolve rbac', { err })\n }\n const { id, page, pageSize, search, name, organizationId, scopeToActiveOrganization, roleIds } = parsed.data\n if (userIdFilter.kind === 'none') {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n const filters: any[] = [{ deletedAt: null }]\n const actorTenantId = auth.tenantId ? String(auth.tenantId) : null\n let effectiveTenantId: string | null = null\n let effectiveOrganizationIds: string[] | null = null\n let effectiveSelectedOrganizationId: string | null = null\n let usesSelectedTenantScope = false\n if (!isSuperAdmin) {\n if (!actorTenantId) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n effectiveTenantId = actorTenantId\n const superAdminUserIds = await listSuperAdminUserIds(em, actorTenantId)\n if (superAdminUserIds.size) {\n filters.push({ id: { $nin: Array.from(superAdminUserIds) as any } })\n }\n } else {\n const selectedTenantId = getSelectedTenantFromRequest(req)\n if (typeof selectedTenantId === 'string' && selectedTenantId.trim().length > 0) {\n const scope = await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: selectedTenantId.trim(),\n })\n if (!scope.tenantId) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n effectiveTenantId = scope.tenantId\n effectiveSelectedOrganizationId = scope.selectedId\n usesSelectedTenantScope = true\n if (Array.isArray(scope.filterIds)) {\n if (scope.filterIds.length === 0) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n effectiveOrganizationIds = scope.filterIds\n }\n }\n }\n if (effectiveTenantId) {\n filters.push({ tenantId: effectiveTenantId })\n }\n if (effectiveOrganizationIds) {\n filters.push({ organizationId: { $in: effectiveOrganizationIds as any } })\n }\n const scopeOrganizationId = usesSelectedTenantScope\n ? effectiveSelectedOrganizationId\n : auth.orgId ?? null\n if (organizationId) filters.push({ organizationId })\n // Recipient/assignee pickers scope to the caller's active organization so they never\n // suggest users outside it. A message composed here is stamped with the caller's\n // active org (auth.orgId), and the message detail endpoint enforces\n // hasOrganizationAccess(scope.organizationId, message.organizationId); scoping the\n // suggestions to the same org keeps a picked recipient able to open what they were sent.\n if (scopeToActiveOrganization) filters.push({ organizationId: auth.orgId ?? null })\n const trimmedName = typeof name === 'string' ? name.trim() : ''\n if (trimmedName) {\n const searchPattern = `%${escapeLikePattern(trimmedName)}%`\n const displayNameFilters: UserListFilter[] = [{ name: { $ilike: searchPattern } }]\n const nameTokenScope: string | null | undefined = isSuperAdmin ? (effectiveTenantId ?? undefined) : auth.tenantId ?? null\n const matchedDisplayNameIds = await findUserIdsBySearchTokens(em, E.auth.user, trimmedName, nameTokenScope, 'name')\n if (matchedDisplayNameIds && matchedDisplayNameIds.length) {\n displayNameFilters.push({ id: { $in: matchedDisplayNameIds } })\n }\n filters.push(displayNameFilters.length > 1 ? { $or: displayNameFilters } : displayNameFilters[0])\n }\n // `?id=` and `?ids=` are already intersected by resolveUserIdFilter.\n let idFilter: Set<string> | null = userIdFilter.kind === 'ids' ? new Set(userIdFilter.ids) : null\n if (Array.isArray(roleIds) && roleIds.length > 0) {\n const uniqueRoleIds = Array.from(new Set(roleIds))\n const linksForRoles = await em.find(\n UserRole,\n buildRoleLinkFilter(uniqueRoleIds, filters, idFilter) as any,\n )\n const roleUserIds = new Set<string>()\n for (const link of linksForRoles) {\n const uid = String((link as any).user?.id || (link as any).user || '')\n if (uid) roleUserIds.add(uid)\n }\n if (roleUserIds.size === 0) return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n if (idFilter) {\n // buildRoleLinkFilter already constrains the lookup to `user.id IN idFilter`, so this\n // intersection is enforced database-side; the loop stays as an application-level backstop\n // so the `?id=` + `?roleId=` contract does not depend on that predicate alone.\n for (const uid of Array.from(idFilter)) {\n if (!roleUserIds.has(uid)) idFilter.delete(uid)\n }\n } else {\n idFilter = roleUserIds\n }\n if (!idFilter || idFilter.size === 0) return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n const trimmedSearch = typeof search === 'string' ? search.trim() : ''\n if (trimmedSearch) {\n // Email is encrypted at rest, so plaintext search must go through search_tokens.\n const tenantScope: string | null | undefined = isSuperAdmin ? (effectiveTenantId ?? undefined) : auth.tenantId ?? null\n const searchFilters: any[] = []\n\n const matchedIds = await findUserIdsBySearchTokens(em, E.auth.user, trimmedSearch, tenantScope)\n if (matchedIds && matchedIds.length) {\n searchFilters.push({ id: { $in: matchedIds as any } })\n }\n\n const searchPattern = `%${escapeLikePattern(trimmedSearch)}%`\n const organizationSearchFilters: any[] = [\n { deletedAt: null },\n { name: { $ilike: searchPattern } },\n ]\n if (tenantScope) {\n organizationSearchFilters.push({ tenant: tenantScope })\n }\n const matchingOrganizations = await em.find(\n Organization,\n organizationSearchFilters.length > 1 ? { $and: organizationSearchFilters } : organizationSearchFilters[0],\n )\n const matchingOrganizationIds = matchingOrganizations\n .map((org) => (org?.id ? String(org.id) : null))\n .filter((orgId): orgId is string => typeof orgId === 'string' && orgId.length > 0)\n if (matchingOrganizationIds.length) {\n searchFilters.push({ organizationId: { $in: matchingOrganizationIds as any } })\n }\n\n const roleSearchFilters: any[] = [\n { deletedAt: null },\n { name: { $ilike: searchPattern } },\n ]\n if (tenantScope) {\n roleSearchFilters.push({ $or: [{ tenantId: tenantScope }, { tenantId: null }] })\n }\n const matchingRoles = await em.find(\n Role,\n roleSearchFilters.length > 1 ? { $and: roleSearchFilters } : roleSearchFilters[0],\n )\n const matchingRoleIds = matchingRoles\n .map((role) => (role?.id ? String(role.id) : null))\n .filter((roleId): roleId is string => typeof roleId === 'string' && roleId.length > 0)\n if (matchingRoleIds.length) {\n const roleSearchLinks = await em.find(\n UserRole,\n buildRoleLinkFilter(matchingRoleIds, filters, idFilter) as any,\n )\n const matchingRoleUserIds = Array.from(new Set(\n roleSearchLinks\n .map((link) => {\n const userRef = (link as any).user\n const userId = userRef?.id ?? userRef\n return userId ? String(userId) : null\n })\n .filter((userId): userId is string => typeof userId === 'string' && userId.length > 0),\n ))\n if (matchingRoleUserIds.length) {\n searchFilters.push({ id: { $in: matchingRoleUserIds as any } })\n }\n }\n\n if (!searchFilters.length) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n\n filters.push(searchFilters.length > 1 ? { $or: searchFilters } : searchFilters[0])\n }\n // `?id=` has no separate path: resolveUserIdFilter folds it into `idFilter`, and a `kind: 'none'`\n // outcome already returned above, so `idFilter` is null only when neither param was supplied.\n if (idFilter && idFilter.size) {\n filters.push({ id: { $in: Array.from(idFilter) as any } })\n }\n const where = filters.length > 1 ? { $and: filters } : filters[0]\n const [rows, count] = await em.findAndCount(User, where, { limit: pageSize, offset: (page - 1) * pageSize })\n const userIds = rows.map((u: any) => u.id)\n const links = userIds.length\n ? await findWithDecryption(\n em,\n UserRole,\n { user: { $in: userIds as any }, deletedAt: null } as any,\n { populate: ['role'] },\n {\n tenantId: effectiveTenantId ?? auth.tenantId ?? null,\n organizationId: scopeOrganizationId,\n },\n )\n : []\n const roleMap: Record<string, string[]> = {}\n const roleIdMap: Record<string, string[]> = {}\n for (const l of links) {\n const uid = String((l as any).user?.id || (l as any).user)\n const rname = String((l as any).role?.name || '')\n const rid = String((l as any).role?.id ?? '')\n if (!roleMap[uid]) roleMap[uid] = []\n if (!roleIdMap[uid]) roleIdMap[uid] = []\n if (rname) roleMap[uid].push(rname)\n if (rid) roleIdMap[uid].push(rid)\n }\n const orgIds = rows\n .map((u: any) => (u.organizationId ? String(u.organizationId) : null))\n .filter((id): id is string => !!id)\n const uniqueOrgIds = Array.from(new Set(orgIds))\n let orgMap: Record<string, string> = {}\n if (uniqueOrgIds.length) {\n const organizations = await em.find(\n Organization,\n { id: { $in: uniqueOrgIds as any }, deletedAt: null },\n )\n orgMap = organizations.reduce<Record<string, string>>((acc, org) => {\n const orgId = org?.id ? String(org.id) : null\n if (!orgId) return acc\n const rawName = (org as any)?.name\n const orgName = typeof rawName === 'string' && rawName.length > 0 ? rawName : orgId\n acc[orgId] = orgName\n return acc\n }, {})\n }\n const tenantIds = rows\n .map((u: any) => (u.tenantId ? String(u.tenantId) : null))\n .filter((id): id is string => !!id)\n const uniqueTenantIds = Array.from(new Set(tenantIds))\n let tenantMap: Record<string, string> = {}\n if (uniqueTenantIds.length) {\n const tenants = await em.find(\n Tenant,\n { id: { $in: uniqueTenantIds as any }, deletedAt: null },\n )\n tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tenantId = tenant?.id ? String(tenant.id) : null\n if (!tenantId) return acc\n const rawName = (tenant as any)?.name\n const tenantName = typeof rawName === 'string' && rawName.length > 0 ? rawName : tenantId\n acc[tenantId] = tenantName\n return acc\n }, {})\n }\n const tenantByUser: Record<string, string | null> = {}\n const organizationByUser: Record<string, string | null> = {}\n for (const u of rows) {\n const uid = String(u.id)\n tenantByUser[uid] = u.tenantId ? String(u.tenantId) : null\n organizationByUser[uid] = u.organizationId ? String(u.organizationId) : null\n }\n const cfByUser = userIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.auth.user,\n recordIds: userIds.map(String),\n tenantIdByRecord: tenantByUser,\n organizationIdByRecord: organizationByUser,\n tenantFallbacks: effectiveTenantId ? [effectiveTenantId] : auth.tenantId ? [auth.tenantId] : [],\n })\n : {}\n\n const items = rows.map((u: any) => {\n const uid = String(u.id)\n const orgId = u.organizationId ? String(u.organizationId) : null\n return {\n id: uid,\n email: String(u.email),\n name: u.name ? String(u.name) : null,\n organizationId: orgId,\n organizationName: orgId ? orgMap[orgId] ?? orgId : null,\n tenantId: u.tenantId ? String(u.tenantId) : null,\n tenantName: u.tenantId ? tenantMap[String(u.tenantId)] ?? String(u.tenantId) : null,\n roles: roleMap[uid] || [],\n roleIds: roleIdMap[uid] || [],\n ...(id ? { hasPassword: !!u.passwordHash } : {}),\n isConfirmed: u.isConfirmed !== false,\n updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : null,\n ...(cfByUser[uid] || {}),\n }\n })\n const totalPages = Math.max(1, Math.ceil(count / pageSize))\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'auth.user',\n organizationId: effectiveSelectedOrganizationId,\n tenantId: effectiveTenantId ?? auth.tenantId ?? null,\n query: parsed.data,\n accessType: id ? 'read:item' : undefined,\n })\n return NextResponse.json({ items, total: count, totalPages, isSuperAdmin })\n}\n\nexport const POST = async (req: Request) => {\n return crud.POST(req)\n}\n\nexport const PUT = async (req: Request) => {\n return crud.PUT(req)\n}\n\nexport const DELETE = async (req: Request) => {\n const targetId = new URL(req.url).searchParams.get('id')\n if (targetId) {\n try {\n await assertCanModifySuperAdminTarget(req, targetId)\n await assertCanAccessUserTarget(req, targetId)\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n }\n return crud.DELETE(req)\n}\n\nasync function findUserIdsBySearchTokens(\n em: EntityManager,\n entityType: string,\n search: string,\n tenantScope: string | null | undefined,\n field?: string,\n): Promise<string[] | null> {\n return findEntityIdsBySearchTokensCompat({\n db: em.getKysely<SearchTokenDatabase>(),\n entityType,\n query: search,\n fields: field ? [field] : undefined,\n scope: { tenantId: tenantScope },\n })\n}\n\nasync function assertCanModifySuperAdminTarget(req: Request, targetUserId: string) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n await assertActorCanModifySuperAdminUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId,\n })\n}\n\nasync function assertCanAccessUserTarget(req: Request, targetUserId: string) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const organizationScope = await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: auth.tenantId ?? null,\n })\n await assertActorCanAccessUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId,\n organizationScope,\n })\n}\n\nfunction resolveDeleteTargetId(parsed: unknown, raw: unknown): string | null {\n const fromParsed = readId((parsed as Record<string, unknown> | null | undefined))\n if (fromParsed) return fromParsed\n const rawRecord = raw as { body?: Record<string, unknown>; query?: Record<string, unknown> } | null | undefined\n return readId(rawRecord?.query) ?? readId(rawRecord?.body)\n}\n\nfunction readId(record: Record<string, unknown> | null | undefined): string | null {\n const value = record?.id\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nasync function assertCanAssignRoles(req: Request, roles: unknown, payload: Record<string, unknown>) {\n if (!Array.isArray(roles)) return\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const tenantId = await resolveTargetTenantIdForRoleGrant(em, payload, auth.tenantId ?? null)\n await assertActorCanGrantRoleTokens({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId,\n organizationId: auth.orgId ?? null,\n roleTokens: roles,\n })\n}\n\nasync function assertCanAssignUserDestination(req: Request, payload: Record<string, unknown>): Promise<boolean> {\n const organizationId = typeof payload.organizationId === 'string' ? payload.organizationId : null\n const targetUserId = typeof payload.id === 'string' ? payload.id : null\n if (!organizationId || !targetUserId) return false\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const targetUser = await findOneWithDecryption(\n em,\n User,\n { id: targetUserId, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n if (!targetUser) return false\n const organization = await findOneWithDecryption(\n em,\n Organization,\n { id: organizationId },\n { populate: ['tenant'] },\n { tenantId: null, organizationId },\n )\n if (!organization) return throwUserDestinationOrganizationNotFound(400)\n const destinationTenantId = organization.tenant?.id ? String(organization.tenant.id) : null\n if (!destinationTenantId) return throwUserDestinationOrganizationNotFound(400)\n const currentOrganizationId = targetUser.organizationId ? String(targetUser.organizationId) : null\n const currentTenantId = targetUser.tenantId ? String(targetUser.tenantId) : null\n if (currentOrganizationId === organizationId && currentTenantId === destinationTenantId) {\n return false\n }\n const roles = await resolveUserDestinationRoles({\n em,\n targetUserId,\n destinationTenantId,\n roleTokens: payload.roles,\n })\n const organizationScope = await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: destinationTenantId,\n })\n await assertActorCanAssignUserDestination({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n actorIsSuperAdmin: auth.isSuperAdmin === true,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n allowedOrganizationIds: organizationScope.allowedIds,\n destinationTenantId,\n destinationOrganizationId: organizationId,\n roles,\n })\n return true\n}\n\nasync function resolveTargetTenantIdForRoleGrant(\n em: EntityManager,\n payload: Record<string, unknown>,\n fallbackTenantId: string | null,\n): Promise<string | null> {\n const organizationId = typeof payload.organizationId === 'string' ? payload.organizationId : null\n if (organizationId) {\n const organization = await findOneWithDecryption(\n em,\n Organization,\n { id: organizationId },\n { populate: ['tenant'] },\n { tenantId: null, organizationId },\n )\n return organization?.tenant?.id ? String(organization.tenant.id) : fallbackTenantId\n }\n\n const userId = typeof payload.id === 'string' ? payload.id : null\n if (userId) {\n const user = await findOneWithDecryption(\n em,\n User,\n { id: userId, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n return user?.tenantId ? String(user.tenantId) : fallbackTenantId\n }\n\n return fallbackTenantId\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'User management',\n methods: {\n GET: {\n summary: 'List users',\n description:\n 'Non-superadmins with auth.users.list receive a tenant-wide staff directory by default, excluding superadmin accounts. Neither the active organization nor the caller\\'s organization visibility list narrows this default. Directory visibility does not grant access to organization-owned records. Super administrators may scope the response via the topbar tenant and organization selection; without a selected tenant, their list can span tenants. Search matches email, organization name, and role name. Pass organizationId for an exact organization filter, or scopeToActiveOrganization=1 to restrict results to the caller\\'s exact active organization (used by recipient/assignee pickers); when the caller has no active organization, this selects only users with no organization. Both filters apply when supplied together. These are optional list filters, not authorization boundaries. Pass ids=<uuid>,<uuid> (max 100) to resolve a known set of users in one request, for example to label a list of foreign keys; it intersects with id and roleId, and a supplied ids value that contains no valid identifier matches nothing.',\n query: querySchema,\n responses: [\n { status: 200, description: 'User collection', schema: userListResponseSchema },\n ],\n },\n POST: {\n summary: 'Create user',\n description: 'Creates a new confirmed user within the specified organization, optional display name, and optional roles. The organization must be within the caller\\'s descendant-expanded organization scope, whether or not roles are sent, and the roles must belong to its tenant and be grantable by the caller.',\n requestBody: {\n contentType: 'application/json',\n schema: userCreateSchema,\n },\n responses: [\n {\n status: 201,\n description: 'User created',\n schema: z.object({ id: z.string().uuid() }),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or duplicate email', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Attempted to assign privileged roles, or the organization is outside the caller\\'s organization scope', schema: errorResponseSchema },\n ],\n },\n PUT: {\n summary: 'Update user',\n description:\n 'Updates profile fields including display name, organization assignment, credentials, or role memberships. A destination organization must be within the caller\\'s descendant-expanded organization scope. Retained and newly assigned roles must belong to the destination tenant and be grantable by the caller. Setting isConfirmed=false deactivates the account: the user can no longer sign in and every active session is revoked; isConfirmed=true reactivates it. A tenant cannot drop below a protected role\\'s minimum active holder count, so revoking the role from, deactivating, moving, or deleting the last active administrator is rejected.',\n requestBody: {\n contentType: 'application/json',\n schema: userUpdateSchema,\n },\n responses: [\n { status: 200, description: 'User updated', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload, duplicate email, or the update would remove the last active holder of a protected role', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Destination organization is outside caller scope, or a retained or assigned role is not grantable', schema: errorResponseSchema },\n { status: 404, description: 'User or destination organization not found in the caller tenant scope', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Delete user',\n description: 'Deletes a user by identifier. Rejected when the target is the last active holder of a protected role in the tenant. Undo support is provided via the command bus.',\n query: z.object({ id: z.string().uuid().describe('User identifier') }),\n responses: [\n { status: 200, description: 'User deleted', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'User cannot be deleted, or is the last active holder of a protected role', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'User not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { User, Role, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { userCrudEvents, userCrudIndexer } from '@open-mercato/core/modules/auth/commands/users'\nimport {\n assertActorCanAccessUserTarget,\n assertActorCanAssignUserDestination,\n assertActorCanGrantRoleTokens,\n assertActorCanModifySuperAdminUserTarget,\n listSuperAdminUserIds,\n resolveUserDestinationRoles,\n throwUserDestinationOrganizationNotFound,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanFlag } from '@open-mercato/shared/lib/boolean'\nimport { MAX_USER_LOOKUP_IDS, resolveUserIdFilter } from '@open-mercato/core/modules/auth/lib/userIdFilter'\nimport { findEntityIdsBySearchTokensCompat, type SearchTokenDatabase } from '@open-mercato/shared/lib/search/tokenLookup'\nimport { normalizeDisplayNameInput } from '@open-mercato/core/modules/auth/lib/displayName'\nimport {\n getSelectedTenantFromRequest,\n resolveOrganizationScopeForRequest,\n} from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'users' })\n\nconst querySchema = z.object({\n id: z.string().uuid().optional(),\n ids: z.string().optional().describe('Comma-separated user identifiers, at most 100'),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n name: z.string().optional(),\n organizationId: z.string().uuid().optional(),\n scopeToActiveOrganization: z.boolean().optional(),\n roleIds: z.array(z.string().uuid()).optional(),\n}).passthrough()\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst passwordSchema = buildPasswordSchema()\n\nconst displayNameSchema = z.preprocess(\n normalizeDisplayNameInput,\n z.string().trim().min(1).max(120).nullable().optional(),\n)\n\nconst userCreateSchema = z.object({\n email: z.string().email(),\n name: displayNameSchema,\n password: passwordSchema.optional(),\n sendInviteEmail: z.boolean().optional(),\n organizationId: z.string().uuid(),\n roles: z.array(z.string()).optional(),\n}).refine(\n (data) => data.password || data.sendInviteEmail,\n { message: 'Either password or sendInviteEmail is required', path: ['password'] },\n)\n\nconst userUpdateSchema = z.object({\n id: z.string().uuid(),\n email: z.string().email().optional(),\n name: displayNameSchema,\n password: passwordSchema.optional(),\n organizationId: z.string().uuid().optional(),\n roles: z.array(z.string()).optional(),\n isConfirmed: z.boolean().optional(),\n})\n\nconst userListItemSchema = z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n name: z.string().nullable(),\n organizationId: z.string().uuid().nullable(),\n organizationName: z.string().nullable(),\n tenantId: z.string().uuid().nullable(),\n tenantName: z.string().nullable(),\n roles: z.array(z.string()),\n roleIds: z.array(z.string().uuid()).optional(),\n hasPassword: z.boolean().optional(),\n isConfirmed: z.boolean(),\n updatedAt: z.string().nullable().optional(),\n})\n\nconst userListResponseSchema = z.object({\n items: z.array(userListItemSchema),\n total: z.number().int().nonnegative(),\n totalPages: z.number().int().positive(),\n isSuperAdmin: z.boolean().optional(),\n})\n\nconst okResponseSchema = z.object({ ok: z.literal(true) })\n\nconst errorResponseSchema = z.object({ error: z.string() })\n\ntype CrudInput = Record<string, unknown>\ntype UserListFilter = Record<string, unknown>\n\n// UserRole carries no tenant/organization columns of its own, so the caller's scope has to be\n// expressed as a predicate on the `user` relation \u2014 MikroORM compiles that into a database-side\n// join against `users` with the scope predicates in the WHERE clause, keeping the link lookup\n// bounded to the scope instead of every tenant holding the role.\nfunction buildRoleLinkFilter(\n roleIdList: string[],\n userScope: UserListFilter[],\n candidateUserIds: Set<string> | null,\n): UserListFilter {\n const scope = candidateUserIds\n ? [...userScope, { id: { $in: Array.from(candidateUserIds) } }]\n : userScope\n return {\n role: { $in: roleIdList },\n deletedAt: null,\n user: scope.length > 1 ? { $and: scope } : scope[0],\n }\n}\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.users.list'] },\n POST: { requireAuth: true, requireFeatures: ['auth.users.create'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.users.edit'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.users.delete'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: User,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: userCrudEvents,\n indexer: userCrudIndexer,\n actions: {\n create: {\n commandId: 'auth.users.create',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n if (ctx.request) {\n await assertCanAssignRoles(ctx.request, parsed.roles, parsed)\n }\n return parsed\n },\n response: ({ result }) => ({\n id: String(result.user.id),\n ...(result.warning ? { _warning: result.warning } : {}),\n }),\n status: 201,\n },\n update: {\n commandId: 'auth.users.update',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n if (ctx.request) {\n if (typeof parsed.id === 'string' && parsed.id.length) {\n await assertCanModifySuperAdminTarget(ctx.request, parsed.id)\n await assertCanAccessUserTarget(ctx.request, parsed.id)\n }\n if (typeof parsed.organizationId === 'string' && parsed.organizationId.length) {\n const destinationChanged = await assertCanAssignUserDestination(ctx.request, parsed)\n if (!destinationChanged) {\n await assertCanAssignRoles(ctx.request, parsed.roles, parsed)\n }\n } else {\n await assertCanAssignRoles(ctx.request, parsed.roles, parsed)\n }\n }\n return parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'auth.users.delete',\n mapInput: async ({ parsed, raw, ctx }) => {\n const targetId = resolveDeleteTargetId(parsed, raw)\n if (ctx.request && targetId) {\n await assertCanModifySuperAdminTarget(ctx.request, targetId)\n await assertCanAccessUserTarget(ctx.request, targetId)\n }\n return parsed\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const url = new URL(req.url)\n const rawRoleIds = url.searchParams.getAll('roleId').filter((id): id is string => typeof id === 'string' && id.trim().length > 0)\n // Accept both the repeated (`?ids=a&ids=b`) and comma-joined (`?ids=a,b`) spellings.\n const rawIds = url.searchParams.getAll('ids').join(',') || undefined\n const userIdFilter = resolveUserIdFilter(rawIds, url.searchParams.get('id'))\n const parsed = querySchema.safeParse({\n id: url.searchParams.get('id') || undefined,\n ids: rawIds,\n page: url.searchParams.get('page') || undefined,\n // A caller resolving a batch of ids wants all of them; without this the default page of 50\n // would silently truncate a 100-id lookup.\n pageSize: url.searchParams.get('pageSize')\n || (rawIds && userIdFilter.kind === 'ids' ? String(Math.min(userIdFilter.ids.length, MAX_USER_LOOKUP_IDS)) : undefined),\n search: url.searchParams.get('search') || undefined,\n name: url.searchParams.get('name') || undefined,\n organizationId: url.searchParams.get('organizationId') || undefined,\n scopeToActiveOrganization: parseBooleanFlag(url.searchParams.get('scopeToActiveOrganization') || undefined),\n roleIds: rawRoleIds.length ? rawRoleIds : undefined,\n })\n if (!parsed.success) return NextResponse.json({ items: [], total: 0, totalPages: 1 })\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n let isSuperAdmin = auth.isSuperAdmin === true\n try {\n if (auth.sub) {\n const rbacService = container.resolve('rbacService') as any\n const acl = await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })\n isSuperAdmin = isSuperAdmin || !!acl?.isSuperAdmin\n }\n } catch (err) {\n logger.error('Failed to resolve rbac', { err })\n }\n const { id, page, pageSize, search, name, organizationId, scopeToActiveOrganization, roleIds } = parsed.data\n if (userIdFilter.kind === 'none') {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n const filters: any[] = [{ deletedAt: null }]\n const actorTenantId = auth.tenantId ? String(auth.tenantId) : null\n let effectiveTenantId: string | null = null\n let effectiveOrganizationIds: string[] | null = null\n let effectiveSelectedOrganizationId: string | null = null\n let usesSelectedTenantScope = false\n if (!isSuperAdmin) {\n if (!actorTenantId) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n effectiveTenantId = actorTenantId\n const superAdminUserIds = await listSuperAdminUserIds(em, actorTenantId)\n if (superAdminUserIds.size) {\n filters.push({ id: { $nin: Array.from(superAdminUserIds) as any } })\n }\n } else {\n const selectedTenantId = getSelectedTenantFromRequest(req)\n if (typeof selectedTenantId === 'string' && selectedTenantId.trim().length > 0) {\n const scope = await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: selectedTenantId.trim(),\n })\n if (!scope.tenantId) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n effectiveTenantId = scope.tenantId\n effectiveSelectedOrganizationId = scope.selectedId\n usesSelectedTenantScope = true\n if (Array.isArray(scope.filterIds)) {\n if (scope.filterIds.length === 0) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n effectiveOrganizationIds = scope.filterIds\n }\n }\n }\n if (effectiveTenantId) {\n filters.push({ tenantId: effectiveTenantId })\n }\n if (effectiveOrganizationIds) {\n filters.push({ organizationId: { $in: effectiveOrganizationIds as any } })\n }\n const scopeOrganizationId = usesSelectedTenantScope\n ? effectiveSelectedOrganizationId\n : auth.orgId ?? null\n if (organizationId) filters.push({ organizationId })\n // Recipient/assignee pickers scope to the caller's active organization so they never\n // suggest users outside it. A message composed here is stamped with the caller's\n // active org (auth.orgId), and the message detail endpoint enforces\n // hasOrganizationAccess(scope.organizationId, message.organizationId); scoping the\n // suggestions to the same org keeps a picked recipient able to open what they were sent.\n if (scopeToActiveOrganization) filters.push({ organizationId: auth.orgId ?? null })\n const trimmedName = typeof name === 'string' ? name.trim() : ''\n if (trimmedName) {\n const searchPattern = `%${escapeLikePattern(trimmedName)}%`\n const displayNameFilters: UserListFilter[] = [{ name: { $ilike: searchPattern } }]\n const nameTokenScope: string | null | undefined = isSuperAdmin ? (effectiveTenantId ?? undefined) : auth.tenantId ?? null\n const matchedDisplayNameIds = await findUserIdsBySearchTokens(em, E.auth.user, trimmedName, nameTokenScope, 'name')\n if (matchedDisplayNameIds && matchedDisplayNameIds.length) {\n displayNameFilters.push({ id: { $in: matchedDisplayNameIds } })\n }\n filters.push(displayNameFilters.length > 1 ? { $or: displayNameFilters } : displayNameFilters[0])\n }\n // `?id=` and `?ids=` are already intersected by resolveUserIdFilter.\n let idFilter: Set<string> | null = userIdFilter.kind === 'ids' ? new Set(userIdFilter.ids) : null\n if (Array.isArray(roleIds) && roleIds.length > 0) {\n const uniqueRoleIds = Array.from(new Set(roleIds))\n const linksForRoles = await em.find(\n UserRole,\n buildRoleLinkFilter(uniqueRoleIds, filters, idFilter) as any,\n )\n const roleUserIds = new Set<string>()\n for (const link of linksForRoles) {\n const uid = String((link as any).user?.id || (link as any).user || '')\n if (uid) roleUserIds.add(uid)\n }\n if (roleUserIds.size === 0) return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n if (idFilter) {\n // buildRoleLinkFilter already constrains the lookup to `user.id IN idFilter`, so this\n // intersection is enforced database-side; the loop stays as an application-level backstop\n // so the `?id=` + `?roleId=` contract does not depend on that predicate alone.\n for (const uid of Array.from(idFilter)) {\n if (!roleUserIds.has(uid)) idFilter.delete(uid)\n }\n } else {\n idFilter = roleUserIds\n }\n if (!idFilter || idFilter.size === 0) return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n const trimmedSearch = typeof search === 'string' ? search.trim() : ''\n if (trimmedSearch) {\n // Email is encrypted at rest, so plaintext search must go through search_tokens.\n const tenantScope: string | null | undefined = isSuperAdmin ? (effectiveTenantId ?? undefined) : auth.tenantId ?? null\n const searchFilters: any[] = []\n\n const matchedIds = await findUserIdsBySearchTokens(em, E.auth.user, trimmedSearch, tenantScope)\n if (matchedIds && matchedIds.length) {\n searchFilters.push({ id: { $in: matchedIds as any } })\n }\n\n const searchPattern = `%${escapeLikePattern(trimmedSearch)}%`\n const organizationSearchFilters: any[] = [\n { deletedAt: null },\n { name: { $ilike: searchPattern } },\n ]\n if (tenantScope) {\n organizationSearchFilters.push({ tenant: tenantScope })\n }\n const matchingOrganizations = await em.find(\n Organization,\n organizationSearchFilters.length > 1 ? { $and: organizationSearchFilters } : organizationSearchFilters[0],\n )\n const matchingOrganizationIds = matchingOrganizations\n .map((org) => (org?.id ? String(org.id) : null))\n .filter((orgId): orgId is string => typeof orgId === 'string' && orgId.length > 0)\n if (matchingOrganizationIds.length) {\n searchFilters.push({ organizationId: { $in: matchingOrganizationIds as any } })\n }\n\n const roleSearchFilters: any[] = [\n { deletedAt: null },\n { name: { $ilike: searchPattern } },\n ]\n if (tenantScope) {\n roleSearchFilters.push({ $or: [{ tenantId: tenantScope }, { tenantId: null }] })\n }\n const matchingRoles = await em.find(\n Role,\n roleSearchFilters.length > 1 ? { $and: roleSearchFilters } : roleSearchFilters[0],\n )\n const matchingRoleIds = matchingRoles\n .map((role) => (role?.id ? String(role.id) : null))\n .filter((roleId): roleId is string => typeof roleId === 'string' && roleId.length > 0)\n if (matchingRoleIds.length) {\n const roleSearchLinks = await em.find(\n UserRole,\n buildRoleLinkFilter(matchingRoleIds, filters, idFilter) as any,\n )\n const matchingRoleUserIds = Array.from(new Set(\n roleSearchLinks\n .map((link) => {\n const userRef = (link as any).user\n const userId = userRef?.id ?? userRef\n return userId ? String(userId) : null\n })\n .filter((userId): userId is string => typeof userId === 'string' && userId.length > 0),\n ))\n if (matchingRoleUserIds.length) {\n searchFilters.push({ id: { $in: matchingRoleUserIds as any } })\n }\n }\n\n if (!searchFilters.length) {\n return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })\n }\n\n filters.push(searchFilters.length > 1 ? { $or: searchFilters } : searchFilters[0])\n }\n // `?id=` has no separate path: resolveUserIdFilter folds it into `idFilter`, and a `kind: 'none'`\n // outcome already returned above, so `idFilter` is null only when neither param was supplied.\n if (idFilter && idFilter.size) {\n filters.push({ id: { $in: Array.from(idFilter) as any } })\n }\n const where = filters.length > 1 ? { $and: filters } : filters[0]\n const [rows, count] = await em.findAndCount(User, where, { limit: pageSize, offset: (page - 1) * pageSize })\n const userIds = rows.map((u: any) => u.id)\n const links = userIds.length\n ? await findWithDecryption(\n em,\n UserRole,\n { user: { $in: userIds as any }, deletedAt: null } as any,\n { populate: ['role'] },\n {\n tenantId: effectiveTenantId,\n organizationId: scopeOrganizationId,\n },\n )\n : []\n const roleMap: Record<string, string[]> = {}\n const roleIdMap: Record<string, string[]> = {}\n for (const l of links) {\n const uid = String((l as any).user?.id || (l as any).user)\n const rname = String((l as any).role?.name || '')\n const rid = String((l as any).role?.id ?? '')\n if (!roleMap[uid]) roleMap[uid] = []\n if (!roleIdMap[uid]) roleIdMap[uid] = []\n if (rname) roleMap[uid].push(rname)\n if (rid) roleIdMap[uid].push(rid)\n }\n const orgIds = rows\n .map((u: any) => (u.organizationId ? String(u.organizationId) : null))\n .filter((id): id is string => !!id)\n const uniqueOrgIds = Array.from(new Set(orgIds))\n let orgMap: Record<string, string> = {}\n if (uniqueOrgIds.length) {\n const organizations = await em.find(\n Organization,\n { id: { $in: uniqueOrgIds as any }, deletedAt: null },\n )\n orgMap = organizations.reduce<Record<string, string>>((acc, org) => {\n const orgId = org?.id ? String(org.id) : null\n if (!orgId) return acc\n const rawName = (org as any)?.name\n const orgName = typeof rawName === 'string' && rawName.length > 0 ? rawName : orgId\n acc[orgId] = orgName\n return acc\n }, {})\n }\n const tenantIds = rows\n .map((u: any) => (u.tenantId ? String(u.tenantId) : null))\n .filter((id): id is string => !!id)\n const uniqueTenantIds = Array.from(new Set(tenantIds))\n let tenantMap: Record<string, string> = {}\n if (uniqueTenantIds.length) {\n const tenants = await em.find(\n Tenant,\n { id: { $in: uniqueTenantIds as any }, deletedAt: null },\n )\n tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tenantId = tenant?.id ? String(tenant.id) : null\n if (!tenantId) return acc\n const rawName = (tenant as any)?.name\n const tenantName = typeof rawName === 'string' && rawName.length > 0 ? rawName : tenantId\n acc[tenantId] = tenantName\n return acc\n }, {})\n }\n const tenantByUser: Record<string, string | null> = {}\n const organizationByUser: Record<string, string | null> = {}\n for (const u of rows) {\n const uid = String(u.id)\n tenantByUser[uid] = u.tenantId ? String(u.tenantId) : null\n organizationByUser[uid] = u.organizationId ? String(u.organizationId) : null\n }\n const cfByUser = userIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.auth.user,\n recordIds: userIds.map(String),\n tenantIdByRecord: tenantByUser,\n organizationIdByRecord: organizationByUser,\n tenantFallbacks: effectiveTenantId ? [effectiveTenantId] : auth.tenantId ? [auth.tenantId] : [],\n })\n : {}\n\n const items = rows.map((u: any) => {\n const uid = String(u.id)\n const orgId = u.organizationId ? String(u.organizationId) : null\n return {\n id: uid,\n email: String(u.email),\n name: u.name ? String(u.name) : null,\n organizationId: orgId,\n organizationName: orgId ? orgMap[orgId] ?? orgId : null,\n tenantId: u.tenantId ? String(u.tenantId) : null,\n tenantName: u.tenantId ? tenantMap[String(u.tenantId)] ?? String(u.tenantId) : null,\n roles: roleMap[uid] || [],\n roleIds: roleIdMap[uid] || [],\n ...(id ? { hasPassword: !!u.passwordHash } : {}),\n isConfirmed: u.isConfirmed !== false,\n updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : null,\n ...(cfByUser[uid] || {}),\n }\n })\n const totalPages = Math.max(1, Math.ceil(count / pageSize))\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'auth.user',\n organizationId: effectiveSelectedOrganizationId,\n tenantId: effectiveTenantId ?? auth.tenantId ?? null,\n query: parsed.data,\n accessType: id ? 'read:item' : undefined,\n })\n return NextResponse.json({ items, total: count, totalPages, isSuperAdmin })\n}\n\nexport const POST = async (req: Request) => {\n return crud.POST(req)\n}\n\nexport const PUT = async (req: Request) => {\n return crud.PUT(req)\n}\n\nexport const DELETE = async (req: Request) => {\n const targetId = new URL(req.url).searchParams.get('id')\n if (targetId) {\n try {\n await assertCanModifySuperAdminTarget(req, targetId)\n await assertCanAccessUserTarget(req, targetId)\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n }\n return crud.DELETE(req)\n}\n\nasync function findUserIdsBySearchTokens(\n em: EntityManager,\n entityType: string,\n search: string,\n tenantScope: string | null | undefined,\n field?: string,\n): Promise<string[] | null> {\n return findEntityIdsBySearchTokensCompat({\n db: em.getKysely<SearchTokenDatabase>(),\n entityType,\n query: search,\n fields: field ? [field] : undefined,\n scope: { tenantId: tenantScope },\n })\n}\n\nasync function assertCanModifySuperAdminTarget(req: Request, targetUserId: string) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n await assertActorCanModifySuperAdminUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId,\n })\n}\n\nasync function assertCanAccessUserTarget(req: Request, targetUserId: string) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const organizationScope = await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: auth.tenantId ?? null,\n })\n await assertActorCanAccessUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n targetUserId,\n organizationScope,\n })\n}\n\nfunction resolveDeleteTargetId(parsed: unknown, raw: unknown): string | null {\n const fromParsed = readId((parsed as Record<string, unknown> | null | undefined))\n if (fromParsed) return fromParsed\n const rawRecord = raw as { body?: Record<string, unknown>; query?: Record<string, unknown> } | null | undefined\n return readId(rawRecord?.query) ?? readId(rawRecord?.body)\n}\n\nfunction readId(record: Record<string, unknown> | null | undefined): string | null {\n const value = record?.id\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nasync function assertCanAssignRoles(req: Request, roles: unknown, payload: Record<string, unknown>) {\n if (!Array.isArray(roles)) return\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const tenantId = await resolveTargetTenantIdForRoleGrant(em, payload, auth.tenantId ?? null)\n await assertActorCanGrantRoleTokens({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId,\n organizationId: auth.orgId ?? null,\n roleTokens: roles,\n })\n}\n\nasync function assertCanAssignUserDestination(req: Request, payload: Record<string, unknown>): Promise<boolean> {\n const organizationId = typeof payload.organizationId === 'string' ? payload.organizationId : null\n const targetUserId = typeof payload.id === 'string' ? payload.id : null\n if (!organizationId || !targetUserId) return false\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const targetUser = await findOneWithDecryption(\n em,\n User,\n { id: targetUserId, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n if (!targetUser) return false\n const organization = await findOneWithDecryption(\n em,\n Organization,\n { id: organizationId },\n { populate: ['tenant'] },\n { tenantId: null, organizationId },\n )\n if (!organization) return throwUserDestinationOrganizationNotFound(400)\n const destinationTenantId = organization.tenant?.id ? String(organization.tenant.id) : null\n if (!destinationTenantId) return throwUserDestinationOrganizationNotFound(400)\n const currentOrganizationId = targetUser.organizationId ? String(targetUser.organizationId) : null\n const currentTenantId = targetUser.tenantId ? String(targetUser.tenantId) : null\n if (currentOrganizationId === organizationId && currentTenantId === destinationTenantId) {\n return false\n }\n const roles = await resolveUserDestinationRoles({\n em,\n targetUserId,\n destinationTenantId,\n roleTokens: payload.roles,\n })\n const organizationScope = await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: destinationTenantId,\n })\n await assertActorCanAssignUserDestination({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n actorIsSuperAdmin: auth.isSuperAdmin === true,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n allowedOrganizationIds: organizationScope.allowedIds,\n destinationTenantId,\n destinationOrganizationId: organizationId,\n roles,\n })\n return true\n}\n\nasync function resolveTargetTenantIdForRoleGrant(\n em: EntityManager,\n payload: Record<string, unknown>,\n fallbackTenantId: string | null,\n): Promise<string | null> {\n const organizationId = typeof payload.organizationId === 'string' ? payload.organizationId : null\n if (organizationId) {\n const organization = await findOneWithDecryption(\n em,\n Organization,\n { id: organizationId },\n { populate: ['tenant'] },\n { tenantId: null, organizationId },\n )\n return organization?.tenant?.id ? String(organization.tenant.id) : fallbackTenantId\n }\n\n const userId = typeof payload.id === 'string' ? payload.id : null\n if (userId) {\n const user = await findOneWithDecryption(\n em,\n User,\n { id: userId, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n return user?.tenantId ? String(user.tenantId) : fallbackTenantId\n }\n\n return fallbackTenantId\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'User management',\n methods: {\n GET: {\n summary: 'List users',\n description:\n 'Non-superadmins with auth.users.list receive a tenant-wide staff directory by default, excluding superadmin accounts. Neither the active organization nor the caller\\'s organization visibility list narrows this default. Directory visibility does not grant access to organization-owned records. Super administrators may scope the response via the topbar tenant and organization selection; without a selected tenant, their list can span tenants. Search matches email, organization name, and role name. Pass organizationId for an exact organization filter, or scopeToActiveOrganization=1 to restrict results to the caller\\'s exact active organization (used by recipient/assignee pickers); when the caller has no active organization, this selects only users with no organization. Both filters apply when supplied together. These are optional list filters, not authorization boundaries. Pass ids=<uuid>,<uuid> (max 100) to resolve a known set of users in one request, for example to label a list of foreign keys; it intersects with id and roleId, and a supplied ids value that contains no valid identifier matches nothing.',\n query: querySchema,\n responses: [\n { status: 200, description: 'User collection', schema: userListResponseSchema },\n ],\n },\n POST: {\n summary: 'Create user',\n description: 'Creates a new confirmed user within the specified organization, optional display name, and optional roles. The organization must be within the caller\\'s descendant-expanded organization scope, whether or not roles are sent, and the roles must belong to its tenant and be grantable by the caller.',\n requestBody: {\n contentType: 'application/json',\n schema: userCreateSchema,\n },\n responses: [\n {\n status: 201,\n description: 'User created',\n schema: z.object({ id: z.string().uuid() }),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or duplicate email', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Attempted to assign privileged roles, or the organization is outside the caller\\'s organization scope', schema: errorResponseSchema },\n ],\n },\n PUT: {\n summary: 'Update user',\n description:\n 'Updates profile fields including display name, organization assignment, credentials, or role memberships. A destination organization must be within the caller\\'s descendant-expanded organization scope. Retained and newly assigned roles must belong to the destination tenant and be grantable by the caller. Setting isConfirmed=false deactivates the account: the user can no longer sign in and every active session is revoked; isConfirmed=true reactivates it. A tenant cannot drop below a protected role\\'s minimum active holder count, so revoking the role from, deactivating, moving, or deleting the last active administrator is rejected.',\n requestBody: {\n contentType: 'application/json',\n schema: userUpdateSchema,\n },\n responses: [\n { status: 200, description: 'User updated', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload, duplicate email, or the update would remove the last active holder of a protected role', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Destination organization is outside caller scope, or a retained or assigned role is not grantable', schema: errorResponseSchema },\n { status: 404, description: 'User or destination organization not found in the caller tenant scope', schema: errorResponseSchema },\n ],\n },\n DELETE: {\n summary: 'Delete user',\n description: 'Deletes a user by identifier. Rejected when the target is the last active holder of a protected role in the tenant. Undo support is provided via the command bus.',\n query: z.object({ id: z.string().uuid().describe('User identifier') }),\n responses: [\n { status: 200, description: 'User deleted', schema: okResponseSchema },\n ],\n errors: [\n { status: 400, description: 'User cannot be deleted, or is the last active holder of a protected role', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 404, description: 'User not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["id"]
7
7
  }
@@ -13,6 +13,10 @@ import {
13
13
  customerMagicLinkIpRateLimitConfig
14
14
  } from "@open-mercato/core/modules/customer_accounts/lib/rateLimiter";
15
15
  import { readNormalizedEmailFromJsonRequest } from "@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier";
16
+ import { sendCustomerMagicLinkEmail } from "@open-mercato/core/modules/customer_accounts/lib/authLinkEmails";
17
+ import { createLogger } from "@open-mercato/shared/lib/logger";
18
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
19
+ const logger = createLogger("customer_accounts").child({ component: "magic-link-request" });
16
20
  const metadata = { requireAuth: false };
17
21
  async function POST(req) {
18
22
  const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req);
@@ -49,7 +53,17 @@ async function POST(req) {
49
53
  const customerTokenService = container.resolve("customerTokenService");
50
54
  const user = await customerUserService.findByEmail(email, tenantId);
51
55
  if (user) {
52
- await customerTokenService.createMagicLink(user.id, tenantId);
56
+ const rawToken = await customerTokenService.createMagicLink(user.id, tenantId);
57
+ await sendCustomerMagicLinkEmail({
58
+ container,
59
+ tenantId,
60
+ organizationId: user.organizationId,
61
+ email: user.email,
62
+ rawToken
63
+ }).catch((error) => {
64
+ logger.error("Magic link email failed", { err: error });
65
+ getTelemetryRuntime()?.reportError(error, { module: "customer_accounts", code: "customer_accounts.magic_link_email_failed" });
66
+ });
53
67
  void import("@open-mercato/core/modules/customer_accounts/events").then(
54
68
  ({ emitCustomerAccountsEvent }) => emitCustomerAccountsEvent("customer_accounts.magic_link.requested", {
55
69
  userId: user.id,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customer_accounts/api/magic-link/request.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { magicLinkRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerMagicLinkRateLimitConfig,\n customerMagicLinkIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerMagicLinkIpRateLimitConfig,\n compoundConfig: customerMagicLinkRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: true })\n }\n\n const parsed = magicLinkRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: true })\n }\n\n const { email } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId)\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: true })\n }\n throw err\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (user) {\n await customerTokenService.createMagicLink(user.id, tenantId)\n // Token is stored in DB; email delivery should be handled by a direct service call,\n // NOT via the event bus \u2014 raw tokens must never travel through events.\n void import('@open-mercato/core/modules/customer_accounts/events').then(({ emitCustomerAccountsEvent }) =>\n emitCustomerAccountsEvent('customer_accounts.magic_link.requested', {\n userId: user.id,\n tenantId,\n organizationId: user.organizationId,\n })\n ).catch(() => undefined)\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Request magic link login',\n description: 'Sends a magic link to the customer email. Always returns 200 to prevent enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: magicLinkRequestSchema,\n description: 'Magic link request with email.',\n },\n responses: [\n { status: 200, description: 'Request accepted', schema: successSchema },\n ],\n errors: [\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Request customer magic link',\n description: 'Handles magic link login requests for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,8BAA8B;AAGvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0CAA0C;AAE5C,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,QAAM,iBAAiB,MAAM,mCAAmC,GAAG;AACnE,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,SAAS,uBAAuB,UAAU,IAAI;AACpD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,EAAE,MAAM,IAAI,OAAO;AACzB,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,MAAM,qBAAqB,KAAK,OAAO,KAAK,QAAQ;AACpE,eAAW,QAAQ;AAAA,EACrB,SAAS,KAAK;AACZ,QAAI,eAAe,uBAAuB;AACxC,aAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,IACvC;AACA,UAAM;AAAA,EACR;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,uBAAuB,UAAU,QAAQ,sBAAsB;AAErE,QAAM,OAAO,MAAM,oBAAoB,YAAY,OAAO,QAAQ;AAClE,MAAI,MAAM;AACR,UAAM,qBAAqB,gBAAgB,KAAK,IAAI,QAAQ;AAG5D,SAAK,OAAO,qDAAqD,EAAE;AAAA,MAAK,CAAC,EAAE,0BAA0B,MACnG,0BAA0B,0CAA0C;AAAA,QAClE,QAAQ,KAAK;AAAA,QACb;AAAA,QACA,gBAAgB,KAAK;AAAA,MACvB,CAAC;AAAA,IACH,EAAE,MAAM,MAAM,MAAS;AAAA,EACzB;AAEA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEA,MAAM,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAEtD,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,yBAAyB;AAAA,EAChC,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,cAAc;AAAA,EACxE;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,EAChF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS,EAAE,MAAM,UAAU;AAC7B;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { magicLinkRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerMagicLinkRateLimitConfig,\n customerMagicLinkIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\nimport { sendCustomerMagicLinkEmail } from '@open-mercato/core/modules/customer_accounts/lib/authLinkEmails'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nconst logger = createLogger('customer_accounts').child({ component: 'magic-link-request' })\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerMagicLinkIpRateLimitConfig,\n compoundConfig: customerMagicLinkRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: true })\n }\n\n const parsed = magicLinkRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: true })\n }\n\n const { email } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId)\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: true })\n }\n throw err\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (user) {\n const rawToken = await customerTokenService.createMagicLink(user.id, tenantId)\n await sendCustomerMagicLinkEmail({\n container,\n tenantId,\n organizationId: user.organizationId,\n email: user.email,\n rawToken,\n }).catch((error) => {\n logger.error('Magic link email failed', { err: error })\n getTelemetryRuntime()?.reportError(error, { module: 'customer_accounts', code: 'customer_accounts.magic_link_email_failed' })\n })\n void import('@open-mercato/core/modules/customer_accounts/events').then(({ emitCustomerAccountsEvent }) =>\n emitCustomerAccountsEvent('customer_accounts.magic_link.requested', {\n userId: user.id,\n tenantId,\n organizationId: user.organizationId,\n })\n ).catch(() => undefined)\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Request magic link login',\n description: 'Sends a magic link to the customer email. Always returns 200 to prevent enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: magicLinkRequestSchema,\n description: 'Magic link request with email.',\n },\n responses: [\n { status: 200, description: 'Request accepted', schema: successSchema },\n ],\n errors: [\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Request customer magic link',\n description: 'Handles magic link login requests for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -13,6 +13,10 @@ import {
13
13
  customerPasswordResetIpRateLimitConfig
14
14
  } from "@open-mercato/core/modules/customer_accounts/lib/rateLimiter";
15
15
  import { readNormalizedEmailFromJsonRequest } from "@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier";
16
+ import { sendCustomerPasswordResetEmail } from "@open-mercato/core/modules/customer_accounts/lib/authLinkEmails";
17
+ import { createLogger } from "@open-mercato/shared/lib/logger";
18
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
19
+ const logger = createLogger("customer_accounts").child({ component: "password-reset-request" });
16
20
  const metadata = { requireAuth: false };
17
21
  async function POST(req) {
18
22
  const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req);
@@ -49,7 +53,17 @@ async function POST(req) {
49
53
  const customerTokenService = container.resolve("customerTokenService");
50
54
  const user = await customerUserService.findByEmail(email, tenantId);
51
55
  if (user) {
52
- await customerTokenService.createPasswordReset(user.id, tenantId);
56
+ const rawToken = await customerTokenService.createPasswordReset(user.id, tenantId);
57
+ await sendCustomerPasswordResetEmail({
58
+ container,
59
+ tenantId,
60
+ organizationId: user.organizationId,
61
+ email: user.email,
62
+ rawToken
63
+ }).catch((error) => {
64
+ logger.error("Password reset email failed", { err: error });
65
+ getTelemetryRuntime()?.reportError(error, { module: "customer_accounts", code: "customer_accounts.password_reset_email_failed" });
66
+ });
53
67
  void import("@open-mercato/core/modules/customer_accounts/events").then(
54
68
  ({ emitCustomerAccountsEvent }) => emitCustomerAccountsEvent("customer_accounts.password_reset.requested", {
55
69
  userId: user.id,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customer_accounts/api/password/reset-request.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { passwordResetRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerPasswordResetRateLimitConfig,\n customerPasswordResetIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerPasswordResetIpRateLimitConfig,\n compoundConfig: customerPasswordResetRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: true }) // Always 200\n }\n\n const parsed = passwordResetRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: true }) // Always 200 to prevent enumeration\n }\n\n const { email } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId)\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n // Always return 200 to avoid leaking whether a tenant exists for this host.\n return NextResponse.json({ ok: true })\n }\n throw err\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (user) {\n await customerTokenService.createPasswordReset(user.id, tenantId)\n // Token is stored in DB; email delivery should be handled by a direct service call,\n // NOT via the event bus \u2014 raw tokens must never travel through events.\n void import('@open-mercato/core/modules/customer_accounts/events').then(({ emitCustomerAccountsEvent }) =>\n emitCustomerAccountsEvent('customer_accounts.password_reset.requested', {\n userId: user.id,\n tenantId,\n })\n ).catch(() => undefined)\n }\n\n // Always return 200 to prevent email enumeration\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Request customer password reset',\n description: 'Initiates a password reset flow. Always returns 200 to prevent email enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: passwordResetRequestSchema,\n description: 'Password reset request with email.',\n },\n responses: [\n { status: 200, description: 'Request accepted', schema: successSchema },\n ],\n errors: [\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer password reset request',\n description: 'Handles password reset initiation for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kCAAkC;AAC3C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,8BAA8B;AAGvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0CAA0C;AAE5C,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,QAAM,iBAAiB,MAAM,mCAAmC,GAAG;AACnE,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,SAAS,2BAA2B,UAAU,IAAI;AACxD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC;AAEA,QAAM,EAAE,MAAM,IAAI,OAAO;AACzB,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,MAAM,qBAAqB,KAAK,OAAO,KAAK,QAAQ;AACpE,eAAW,QAAQ;AAAA,EACrB,SAAS,KAAK;AACZ,QAAI,eAAe,uBAAuB;AAExC,aAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,IACvC;AACA,UAAM;AAAA,EACR;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,uBAAuB,UAAU,QAAQ,sBAAsB;AAErE,QAAM,OAAO,MAAM,oBAAoB,YAAY,OAAO,QAAQ;AAClE,MAAI,MAAM;AACR,UAAM,qBAAqB,oBAAoB,KAAK,IAAI,QAAQ;AAGhE,SAAK,OAAO,qDAAqD,EAAE;AAAA,MAAK,CAAC,EAAE,0BAA0B,MACnG,0BAA0B,8CAA8C;AAAA,QACtE,QAAQ,KAAK;AAAA,QACb;AAAA,MACF,CAAC;AAAA,IACH,EAAE,MAAM,MAAM,MAAS;AAAA,EACzB;AAGA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEA,MAAM,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC;AAEtD,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,yBAAyB;AAAA,EAChC,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,cAAc;AAAA,EACxE;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,EAChF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS,EAAE,MAAM,UAAU;AAC7B;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { passwordResetRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerPasswordResetRateLimitConfig,\n customerPasswordResetIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\nimport { sendCustomerPasswordResetEmail } from '@open-mercato/core/modules/customer_accounts/lib/authLinkEmails'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nconst logger = createLogger('customer_accounts').child({ component: 'password-reset-request' })\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerPasswordResetIpRateLimitConfig,\n compoundConfig: customerPasswordResetRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: true }) // Always 200\n }\n\n const parsed = passwordResetRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: true }) // Always 200 to prevent enumeration\n }\n\n const { email } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId)\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n // Always return 200 to avoid leaking whether a tenant exists for this host.\n return NextResponse.json({ ok: true })\n }\n throw err\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n if (user) {\n const rawToken = await customerTokenService.createPasswordReset(user.id, tenantId)\n await sendCustomerPasswordResetEmail({\n container,\n tenantId,\n organizationId: user.organizationId,\n email: user.email,\n rawToken,\n }).catch((error) => {\n logger.error('Password reset email failed', { err: error })\n getTelemetryRuntime()?.reportError(error, { module: 'customer_accounts', code: 'customer_accounts.password_reset_email_failed' })\n })\n void import('@open-mercato/core/modules/customer_accounts/events').then(({ emitCustomerAccountsEvent }) =>\n emitCustomerAccountsEvent('customer_accounts.password_reset.requested', {\n userId: user.id,\n tenantId,\n })\n ).catch(() => undefined)\n }\n\n // Always return 200 to prevent email enumeration\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Request customer password reset',\n description: 'Initiates a password reset flow. Always returns 200 to prevent email enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: passwordResetRequestSchema,\n description: 'Password reset request with email.',\n },\n responses: [\n { status: 200, description: 'Request accepted', schema: successSchema },\n ],\n errors: [\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer password reset request',\n description: 'Handles password reset initiation for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,26 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Html, Head, Preview, Body, Container, Text, Section, Link } from "@react-email/components";
3
+ function CustomerAuthLinkEmail({ linkUrl, copy }) {
4
+ return /* @__PURE__ */ jsxs(Html, { children: [
5
+ /* @__PURE__ */ jsx(Head, {}),
6
+ /* @__PURE__ */ jsx(Preview, { children: copy.preview }),
7
+ /* @__PURE__ */ jsx(Body, { style: body, children: /* @__PURE__ */ jsx(Container, { style: container, children: /* @__PURE__ */ jsxs(Section, { children: [
8
+ /* @__PURE__ */ jsx(Text, { style: title, children: copy.title }),
9
+ /* @__PURE__ */ jsx(Text, { style: paragraph, children: copy.body }),
10
+ /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(Link, { href: linkUrl, style: button, children: copy.cta }) }),
11
+ /* @__PURE__ */ jsx(Text, { style: hint, children: copy.hint })
12
+ ] }) }) })
13
+ ] });
14
+ }
15
+ const body = { backgroundColor: "#f9fafb", margin: 0, padding: "24px 0" };
16
+ const container = { backgroundColor: "#ffffff", borderRadius: 12, padding: 24, margin: "0 auto", maxWidth: 520, boxShadow: "0 1px 4px rgba(0,0,0,0.06)" };
17
+ const title = { fontSize: 20, fontWeight: 600, color: "#111827", margin: "0 0 12px" };
18
+ const paragraph = { fontSize: 14, color: "#4b5563", lineHeight: "20px", margin: "0 0 16px" };
19
+ const button = { display: "inline-block", backgroundColor: "#111827", color: "#ffffff", padding: "10px 14px", borderRadius: 8, textDecoration: "none", fontSize: 14 };
20
+ const hint = { fontSize: 12, color: "#6b7280", marginTop: 16 };
21
+ var CustomerAuthLinkEmail_default = CustomerAuthLinkEmail;
22
+ export {
23
+ CustomerAuthLinkEmail,
24
+ CustomerAuthLinkEmail_default as default
25
+ };
26
+ //# sourceMappingURL=CustomerAuthLinkEmail.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customer_accounts/emails/CustomerAuthLinkEmail.tsx"],
4
+ "sourcesContent": ["import * as React from 'react'\nimport { Html, Head, Preview, Body, Container, Text, Section, Link } from '@react-email/components'\n\nexport type CustomerAuthLinkEmailCopy = {\n preview: string\n title: string\n body: string\n cta: string\n hint: string\n}\n\nexport type CustomerAuthLinkEmailProps = {\n linkUrl: string\n copy: CustomerAuthLinkEmailCopy\n}\n\nexport function CustomerAuthLinkEmail({ linkUrl, copy }: CustomerAuthLinkEmailProps) {\n return (\n <Html>\n <Head />\n <Preview>{copy.preview}</Preview>\n <Body style={body}>\n <Container style={container}>\n <Section>\n <Text style={title}>{copy.title}</Text>\n <Text style={paragraph}>{copy.body}</Text>\n <Text>\n <Link href={linkUrl} style={button}>{copy.cta}</Link>\n </Text>\n <Text style={hint}>{copy.hint}</Text>\n </Section>\n </Container>\n </Body>\n </Html>\n )\n}\n\nconst body: React.CSSProperties = { backgroundColor: '#f9fafb', margin: 0, padding: '24px 0' }\nconst container: React.CSSProperties = { backgroundColor: '#ffffff', borderRadius: 12, padding: 24, margin: '0 auto', maxWidth: 520, boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }\nconst title: React.CSSProperties = { fontSize: 20, fontWeight: 600, color: '#111827', margin: '0 0 12px' }\nconst paragraph: React.CSSProperties = { fontSize: 14, color: '#4b5563', lineHeight: '20px', margin: '0 0 16px' }\nconst button: React.CSSProperties = { display: 'inline-block', backgroundColor: '#111827', color: '#ffffff', padding: '10px 14px', borderRadius: 8, textDecoration: 'none', fontSize: 14 }\nconst hint: React.CSSProperties = { fontSize: 12, color: '#6b7280', marginTop: 16 }\n\nexport default CustomerAuthLinkEmail\n"],
5
+ "mappings": "AAmBM,cAII,YAJJ;AAlBN,SAAS,MAAM,MAAM,SAAS,MAAM,WAAW,MAAM,SAAS,YAAY;AAenE,SAAS,sBAAsB,EAAE,SAAS,KAAK,GAA+B;AACnF,SACE,qBAAC,QACC;AAAA,wBAAC,QAAK;AAAA,IACN,oBAAC,WAAS,eAAK,SAAQ;AAAA,IACvB,oBAAC,QAAK,OAAO,MACX,8BAAC,aAAU,OAAO,WAChB,+BAAC,WACC;AAAA,0BAAC,QAAK,OAAO,OAAQ,eAAK,OAAM;AAAA,MAChC,oBAAC,QAAK,OAAO,WAAY,eAAK,MAAK;AAAA,MACnC,oBAAC,QACC,8BAAC,QAAK,MAAM,SAAS,OAAO,QAAS,eAAK,KAAI,GAChD;AAAA,MACA,oBAAC,QAAK,OAAO,MAAO,eAAK,MAAK;AAAA,OAChC,GACF,GACF;AAAA,KACF;AAEJ;AAEA,MAAM,OAA4B,EAAE,iBAAiB,WAAW,QAAQ,GAAG,SAAS,SAAS;AAC7F,MAAM,YAAiC,EAAE,iBAAiB,WAAW,cAAc,IAAI,SAAS,IAAI,QAAQ,UAAU,UAAU,KAAK,WAAW,6BAA6B;AAC7K,MAAM,QAA6B,EAAE,UAAU,IAAI,YAAY,KAAK,OAAO,WAAW,QAAQ,WAAW;AACzG,MAAM,YAAiC,EAAE,UAAU,IAAI,OAAO,WAAW,YAAY,QAAQ,QAAQ,WAAW;AAChH,MAAM,SAA8B,EAAE,SAAS,gBAAgB,iBAAiB,WAAW,OAAO,WAAW,SAAS,aAAa,cAAc,GAAG,gBAAgB,QAAQ,UAAU,GAAG;AACzL,MAAM,OAA4B,EAAE,UAAU,IAAI,OAAO,WAAW,WAAW,GAAG;AAElF,IAAO,gCAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,67 @@
1
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
2
+ import { sendEmail } from "@open-mercato/shared/lib/email/send";
3
+ import CustomerAuthLinkEmail from "@open-mercato/core/modules/customer_accounts/emails/CustomerAuthLinkEmail";
4
+ import { urlForCustomerOrg } from "@open-mercato/core/modules/customer_accounts/lib/customerUrl";
5
+ async function sendCustomerPasswordResetEmail(input) {
6
+ const { translate } = await resolveTranslations();
7
+ const linkUrl = await urlForCustomerOrg(
8
+ input.organizationId,
9
+ `/reset-password?token=${encodeURIComponent(input.rawToken)}`,
10
+ { container: input.container }
11
+ );
12
+ const subject = translate("customer_accounts.passwordReset.email.subject", "Reset your portal password");
13
+ const copy = {
14
+ preview: translate("customer_accounts.passwordReset.email.preview", "Use this link to choose a new portal password."),
15
+ title: translate("customer_accounts.passwordReset.email.title", "Reset your password"),
16
+ body: translate(
17
+ "customer_accounts.passwordReset.email.body",
18
+ "We received a request to reset the password for your portal account. Use this secure link to choose a new one."
19
+ ),
20
+ cta: translate("customer_accounts.passwordReset.email.cta", "Reset password"),
21
+ hint: translate(
22
+ "customer_accounts.passwordReset.email.hint",
23
+ "This link expires in 60 minutes and can be used once. If you did not request a password reset, you can ignore this email."
24
+ )
25
+ };
26
+ await sendEmail({
27
+ to: input.email,
28
+ subject,
29
+ react: CustomerAuthLinkEmail({ linkUrl, copy }),
30
+ tenantId: input.tenantId,
31
+ organizationId: input.organizationId
32
+ });
33
+ }
34
+ async function sendCustomerMagicLinkEmail(input) {
35
+ const { translate } = await resolveTranslations();
36
+ const linkUrl = await urlForCustomerOrg(
37
+ input.organizationId,
38
+ `/magic-link?token=${encodeURIComponent(input.rawToken)}`,
39
+ { container: input.container }
40
+ );
41
+ const subject = translate("customer_accounts.magicLink.email.subject", "Your portal sign-in link");
42
+ const copy = {
43
+ preview: translate("customer_accounts.magicLink.email.preview", "Use this link to sign in to the customer portal."),
44
+ title: translate("customer_accounts.magicLink.email.title", "Sign in to the portal"),
45
+ body: translate(
46
+ "customer_accounts.magicLink.email.body",
47
+ "Use this secure link to sign in to your portal account without a password."
48
+ ),
49
+ cta: translate("customer_accounts.magicLink.email.cta", "Sign in"),
50
+ hint: translate(
51
+ "customer_accounts.magicLink.email.hint",
52
+ "This link expires in 15 minutes and can be used once. If you did not request it, you can ignore this email."
53
+ )
54
+ };
55
+ await sendEmail({
56
+ to: input.email,
57
+ subject,
58
+ react: CustomerAuthLinkEmail({ linkUrl, copy }),
59
+ tenantId: input.tenantId,
60
+ organizationId: input.organizationId
61
+ });
62
+ }
63
+ export {
64
+ sendCustomerMagicLinkEmail,
65
+ sendCustomerPasswordResetEmail
66
+ };
67
+ //# sourceMappingURL=authLinkEmails.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customer_accounts/lib/authLinkEmails.ts"],
4
+ "sourcesContent": ["import type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport CustomerAuthLinkEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerAuthLinkEmail'\nimport { urlForCustomerOrg } from '@open-mercato/core/modules/customer_accounts/lib/customerUrl'\n\nexport type CustomerAuthLinkEmailInput = {\n container: AppContainer\n tenantId: string\n organizationId: string\n email: string\n rawToken: string\n}\n\nexport async function sendCustomerPasswordResetEmail(input: CustomerAuthLinkEmailInput): Promise<void> {\n const { translate } = await resolveTranslations()\n const linkUrl = await urlForCustomerOrg(\n input.organizationId,\n `/reset-password?token=${encodeURIComponent(input.rawToken)}`,\n { container: input.container },\n )\n\n const subject = translate('customer_accounts.passwordReset.email.subject', 'Reset your portal password')\n const copy = {\n preview: translate('customer_accounts.passwordReset.email.preview', 'Use this link to choose a new portal password.'),\n title: translate('customer_accounts.passwordReset.email.title', 'Reset your password'),\n body: translate(\n 'customer_accounts.passwordReset.email.body',\n 'We received a request to reset the password for your portal account. Use this secure link to choose a new one.',\n ),\n cta: translate('customer_accounts.passwordReset.email.cta', 'Reset password'),\n hint: translate(\n 'customer_accounts.passwordReset.email.hint',\n 'This link expires in 60 minutes and can be used once. If you did not request a password reset, you can ignore this email.',\n ),\n }\n\n await sendEmail({\n to: input.email,\n subject,\n react: CustomerAuthLinkEmail({ linkUrl, copy }),\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n}\n\nexport async function sendCustomerMagicLinkEmail(input: CustomerAuthLinkEmailInput): Promise<void> {\n const { translate } = await resolveTranslations()\n const linkUrl = await urlForCustomerOrg(\n input.organizationId,\n `/magic-link?token=${encodeURIComponent(input.rawToken)}`,\n { container: input.container },\n )\n\n const subject = translate('customer_accounts.magicLink.email.subject', 'Your portal sign-in link')\n const copy = {\n preview: translate('customer_accounts.magicLink.email.preview', 'Use this link to sign in to the customer portal.'),\n title: translate('customer_accounts.magicLink.email.title', 'Sign in to the portal'),\n body: translate(\n 'customer_accounts.magicLink.email.body',\n 'Use this secure link to sign in to your portal account without a password.',\n ),\n cta: translate('customer_accounts.magicLink.email.cta', 'Sign in'),\n hint: translate(\n 'customer_accounts.magicLink.email.hint',\n 'This link expires in 15 minutes and can be used once. If you did not request it, you can ignore this email.',\n ),\n }\n\n await sendEmail({\n to: input.email,\n subject,\n react: CustomerAuthLinkEmail({ linkUrl, copy }),\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n}\n"],
5
+ "mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;AAC1B,OAAO,2BAA2B;AAClC,SAAS,yBAAyB;AAUlC,eAAsB,+BAA+B,OAAkD;AACrG,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,UAAU,MAAM;AAAA,IACpB,MAAM;AAAA,IACN,yBAAyB,mBAAmB,MAAM,QAAQ,CAAC;AAAA,IAC3D,EAAE,WAAW,MAAM,UAAU;AAAA,EAC/B;AAEA,QAAM,UAAU,UAAU,iDAAiD,4BAA4B;AACvG,QAAM,OAAO;AAAA,IACX,SAAS,UAAU,iDAAiD,gDAAgD;AAAA,IACpH,OAAO,UAAU,+CAA+C,qBAAqB;AAAA,IACrF,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,IACA,KAAK,UAAU,6CAA6C,gBAAgB;AAAA,IAC5E,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU;AAAA,IACd,IAAI,MAAM;AAAA,IACV;AAAA,IACA,OAAO,sBAAsB,EAAE,SAAS,KAAK,CAAC;AAAA,IAC9C,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AACH;AAEA,eAAsB,2BAA2B,OAAkD;AACjG,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,UAAU,MAAM;AAAA,IACpB,MAAM;AAAA,IACN,qBAAqB,mBAAmB,MAAM,QAAQ,CAAC;AAAA,IACvD,EAAE,WAAW,MAAM,UAAU;AAAA,EAC/B;AAEA,QAAM,UAAU,UAAU,6CAA6C,0BAA0B;AACjG,QAAM,OAAO;AAAA,IACX,SAAS,UAAU,6CAA6C,kDAAkD;AAAA,IAClH,OAAO,UAAU,2CAA2C,uBAAuB;AAAA,IACnF,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,IACA,KAAK,UAAU,yCAAyC,SAAS;AAAA,IACjE,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU;AAAA,IACd,IAAI,MAAM;AAAA,IACV;AAAA,IACA,OAAO,sBAAsB,EAAE,SAAS,KAAK,CAAC;AAAA,IAC9C,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -2,6 +2,9 @@ import { z } from "zod";
2
2
  import { registerCommand } from "@open-mercato/shared/lib/commands";
3
3
  import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
4
4
  import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
5
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
+ import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
7
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
5
8
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
9
  import { Message, MessageObject, MessageRecipient } from "../data/entities.js";
7
10
  import { emitMessagesEvent } from "../events.js";
@@ -304,8 +307,12 @@ const executeActionCommand = {
304
307
  result = commandResult.result ?? {};
305
308
  operationLogEntry = commandResult.logEntry ?? null;
306
309
  } catch (err) {
307
- logger.error("executeActionCommand sub-command failed", { err });
308
310
  await releaseTerminalClaim();
311
+ if (isCrudHttpError(err) || getCommandInterceptorHttpRejection(err)) {
312
+ throw err;
313
+ }
314
+ logger.error("executeActionCommand sub-command failed", { err });
315
+ getTelemetryRuntime()?.reportError(err, { module: "messages", code: "messages.action_failed" });
309
316
  throw new Error("Action failed");
310
317
  }
311
318
  } else if (action.href) {