@open-mercato/core 0.9.1-develop.7519.1.38f51f8dbb → 0.9.1-develop.7536.1.f9a5776a07
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/logout.js +14 -6
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +6 -1
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/catalog/ai-tools/mutation-pack.js +10 -5
- package/dist/modules/catalog/ai-tools/mutation-pack.js.map +2 -2
- package/dist/modules/catalog/ai-tools/types.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js +5 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/commands/currencies.js +399 -137
- package/dist/modules/currencies/commands/currencies.js.map +2 -2
- package/dist/modules/currencies/lib/seeds.js +2 -0
- package/dist/modules/currencies/lib/seeds.js.map +2 -2
- package/dist/modules/customer_accounts/api/login.js +14 -7
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerUserService.js +10 -10
- package/dist/modules/customer_accounts/services/customerUserService.js.map +2 -2
- package/dist/modules/customers/commands/companies.js +10 -3
- package/dist/modules/customers/commands/companies.js.map +2 -2
- package/dist/modules/customers/commands/interactionRestoreDetails.js +75 -0
- package/dist/modules/customers/commands/interactionRestoreDetails.js.map +7 -0
- package/dist/modules/customers/commands/people.js +10 -3
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js +1 -1
- package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js.map +1 -1
- package/dist/modules/forms/api/form-submissions/[id]/submit/route.js +12 -2
- package/dist/modules/forms/api/form-submissions/[id]/submit/route.js.map +2 -2
- package/dist/modules/forms/api/form-submissions/route.js +16 -1
- package/dist/modules/forms/api/form-submissions/route.js.map +2 -2
- package/dist/modules/forms/api/public/submissions/[id]/submit/route.js +6 -1
- package/dist/modules/forms/api/public/submissions/[id]/submit/route.js.map +2 -2
- package/dist/modules/forms/api/runtime-helpers.js +16 -0
- package/dist/modules/forms/api/runtime-helpers.js.map +2 -2
- package/dist/modules/forms/data/validators.js.map +2 -2
- package/dist/modules/forms/lib/portal-subject-policy.js +17 -0
- package/dist/modules/forms/lib/portal-subject-policy.js.map +7 -0
- package/dist/modules/forms/lib/runtime-principal.js +27 -10
- package/dist/modules/forms/lib/runtime-principal.js.map +2 -2
- package/dist/modules/forms/services/distribution-service.js +35 -13
- package/dist/modules/forms/services/distribution-service.js.map +2 -2
- package/dist/modules/forms/services/submission-service.js +4 -0
- package/dist/modules/forms/services/submission-service.js.map +2 -2
- package/dist/modules/staff/commands/leave-requests.js +54 -24
- package/dist/modules/staff/commands/leave-requests.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/attachments/route.js +19 -7
- package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
- package/dist/modules/warranty_claims/commands/claims.js +1 -1
- package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
- package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +6 -6
- package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +2 -2
- package/dist/modules/wms/commands/inventory-actions.js +19 -3
- package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
- package/dist/modules/workflows/lib/async-activity-wait.js +43 -0
- package/dist/modules/workflows/lib/async-activity-wait.js.map +7 -0
- package/dist/modules/workflows/lib/parallel-handler.js +20 -14
- package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +21 -23
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/logout.ts +15 -6
- package/src/modules/auth/api/session/refresh.ts +6 -1
- package/src/modules/catalog/ai-tools/mutation-pack.ts +14 -5
- package/src/modules/catalog/ai-tools/types.ts +1 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +5 -1
- package/src/modules/currencies/commands/currencies.ts +475 -168
- package/src/modules/currencies/i18n/de.json +4 -0
- package/src/modules/currencies/i18n/en.json +4 -0
- package/src/modules/currencies/i18n/es.json +4 -0
- package/src/modules/currencies/i18n/ko.json +4 -0
- package/src/modules/currencies/i18n/pl.json +4 -0
- package/src/modules/currencies/lib/seeds.ts +2 -0
- package/src/modules/customer_accounts/api/login.ts +15 -7
- package/src/modules/customer_accounts/services/customerUserService.ts +34 -10
- package/src/modules/customers/commands/companies.ts +16 -4
- package/src/modules/customers/commands/interactionRestoreDetails.ts +110 -0
- package/src/modules/customers/commands/people.ts +16 -4
- package/src/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.ts +1 -1
- package/src/modules/forms/api/form-submissions/[id]/submit/route.ts +16 -3
- package/src/modules/forms/api/form-submissions/route.ts +21 -2
- package/src/modules/forms/api/public/submissions/[id]/submit/route.ts +7 -0
- package/src/modules/forms/api/runtime-helpers.ts +16 -0
- package/src/modules/forms/data/validators.ts +2 -1
- package/src/modules/forms/i18n/de.json +1 -0
- package/src/modules/forms/i18n/en.json +1 -0
- package/src/modules/forms/i18n/es.json +1 -0
- package/src/modules/forms/i18n/ko.json +1 -0
- package/src/modules/forms/i18n/pl.json +1 -0
- package/src/modules/forms/lib/portal-subject-policy.ts +42 -0
- package/src/modules/forms/lib/runtime-principal.ts +35 -11
- package/src/modules/forms/services/distribution-service.ts +35 -13
- package/src/modules/forms/services/submission-service.ts +12 -0
- package/src/modules/staff/commands/leave-requests.ts +54 -23
- package/src/modules/staff/i18n/de.json +2 -0
- package/src/modules/staff/i18n/en.json +2 -0
- package/src/modules/staff/i18n/es.json +2 -0
- package/src/modules/staff/i18n/ko.json +2 -0
- package/src/modules/staff/i18n/pl.json +2 -0
- package/src/modules/warranty_claims/api/portal/attachments/route.ts +20 -8
- package/src/modules/warranty_claims/commands/claims.ts +1 -1
- package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +5 -5
- package/src/modules/wms/commands/inventory-actions.ts +30 -10
- package/src/modules/workflows/lib/async-activity-wait.ts +71 -0
- package/src/modules/workflows/lib/parallel-handler.ts +24 -15
- package/src/modules/workflows/lib/workflow-executor.ts +24 -27
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/currencies/commands/currencies.ts"],
|
|
4
|
-
"sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { buildChanges, requireId, emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, conflict, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { Currency, ExchangeRate } from '../data/entities'\nimport {\n currencyCreateSchema,\n currencyUpdateSchema,\n currencyDeleteSchema,\n type CurrencyCreateInput,\n type CurrencyUpdateInput,\n type CurrencyDeleteInput,\n} from '../data/validators'\nimport type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { buildCurrencyCommandWhere, ensureCurrencyCommandScope } from './scope'\n\nconst currencyCrudEvents: CrudEventsConfig = {\n module: 'currencies',\n entity: 'currency',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\ntype CurrencySnapshot = {\n id: string\n organizationId: string\n tenantId: string\n code: string\n name: string\n symbol: string | null\n decimalPlaces: number\n thousandsSeparator: string | null\n decimalSeparator: string | null\n isBase: boolean\n isActive: boolean\n createdAt: string\n updatedAt: string\n}\n\ntype CurrencyUndoPayload = UndoPayload<CurrencySnapshot>\n\nasync function loadCurrencySnapshot(\n em: EntityManager,\n id: string,\n ctx: Parameters<typeof buildCurrencyCommandWhere>[0],\n): Promise<CurrencySnapshot | null> {\n const record = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id }),\n )\n if (!record) return null\n ensureCurrencyCommandScope(ctx, record)\n return {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n code: record.code,\n name: record.name,\n symbol: record.symbol ?? null,\n decimalPlaces: record.decimalPlaces,\n thousandsSeparator: record.thousandsSeparator ?? null,\n decimalSeparator: record.decimalSeparator ?? null,\n isBase: !!record.isBase,\n isActive: !!record.isActive,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt.toISOString(),\n }\n}\n\nasync function enforceBaseCurrency(\n em: EntityManager,\n currencyId: string,\n organizationId: string,\n tenantId: string\n): Promise<void> {\n await em.nativeUpdate(\n Currency,\n {\n organizationId,\n tenantId,\n id: { $ne: currencyId },\n isBase: true,\n deletedAt: null,\n },\n { isBase: false, updatedAt: new Date() }\n )\n}\n\nconst createCurrencyCommand: CommandHandler<CurrencyCreateInput, { currencyId: string }> = {\n id: 'currencies.currencies.create',\n async execute(input, ctx) {\n const parsed = currencyCreateSchema.parse(input)\n ensureCurrencyCommandScope(ctx, parsed)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n\n // Check for duplicate code\n const existing = await em.findOne(Currency, {\n code: parsed.code,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n deletedAt: null,\n })\n if (existing) {\n throw conflict('Currency code already exists for this organization.')\n }\n\n const now = new Date()\n const record = em.create(Currency, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n code: parsed.code,\n name: parsed.name,\n symbol: parsed.symbol ?? null,\n decimalPlaces: parsed.decimalPlaces ?? 2,\n thousandsSeparator: parsed.thousandsSeparator ?? null,\n decimalSeparator: parsed.decimalSeparator ?? null,\n isBase: parsed.isBase ?? false,\n isActive: parsed.isActive !== false,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n\n // Demote any existing base currency and insert the new record in one\n // transaction; a partial commit would leave zero or two base currencies.\n try {\n await withAtomicFlush(\n em,\n [\n () =>\n record.isBase\n ? enforceBaseCurrency(em, record.id, record.organizationId, record.tenantId)\n : undefined,\n ],\n { transaction: true },\n )\n } catch (err) {\n if (isUniqueViolation(err, 'currencies_code_scope_unique')) {\n throw conflict('Currency code already exists for this organization.')\n }\n throw err\n }\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return { currencyId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadCurrencySnapshot(em, result.currencyId, ctx)\n },\n buildLog: async ({ snapshots }) => {\n const after = snapshots.after as CurrencySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.create', 'Create currency'),\n resourceKind: 'currencies.currency',\n resourceId: after.id,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotAfter: after,\n payload: { undo: { after } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const after = payload?.after ?? null\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(Currency, { id: after.id })\n if (!record) return\n record.deletedAt = new Date()\n record.isActive = false\n await em.flush()\n },\n redo: makeCreateRedo<Currency, CurrencySnapshot, CurrencyCreateInput, { currencyId: string }>({\n entityClass: Currency,\n buildResult: (entity) => ({ currencyId: entity.id }),\n events: currencyCrudEvents,\n afterRestore: async ({ em, entity }) => {\n if (entity.isBase) {\n await enforceBaseCurrency(em, entity.id, entity.organizationId, entity.tenantId)\n await em.flush()\n }\n },\n }),\n}\n\nconst updateCurrencyCommand: CommandHandler<CurrencyUpdateInput, { currencyId: string }> = {\n id: 'currencies.currencies.update',\n async prepare(input, ctx) {\n requireId(input.id, 'Currency ID is required')\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadCurrencySnapshot(em, input.id, ctx)\n return { before }\n },\n async execute(input, ctx) {\n const parsed = currencyUpdateSchema.parse(input)\n requireId(parsed.id, 'Currency ID is required')\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!record) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, record)\n\n // Check code uniqueness if changing code\n if (parsed.code && parsed.code !== record.code) {\n const existing = await em.findOne(Currency, {\n code: parsed.code,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n id: { $ne: record.id },\n deletedAt: null,\n })\n if (existing) {\n throw conflict('Currency code already exists for this organization.')\n }\n }\n\n const allChanges = buildChanges(record as unknown as Record<string, unknown>, parsed, [\n 'code',\n 'name',\n 'symbol',\n 'decimalPlaces',\n 'thousandsSeparator',\n 'decimalSeparator',\n 'isBase',\n 'isActive',\n ])\n const changes = Object.fromEntries(\n Object.entries(allChanges).filter(([, c]) => c.to !== undefined),\n ) as Record<string, { from: unknown; to: unknown }>\n\n if (Object.keys(changes).length === 0) {\n return { currencyId: record.id }\n }\n\n // Demote any existing base currency and persist the scalar changes in one\n // transaction; a partial commit would leave zero or two base currencies.\n // The scalar mutations live in the first phase so the per-phase flush issues\n // the pending changeset on the managed `record` before `enforceBaseCurrency`\n // runs its interleaved `nativeUpdate` (which would otherwise drop the pending\n // UPDATE under v7).\n await withAtomicFlush(\n em,\n [\n () => {\n for (const [key, change] of Object.entries(changes)) {\n ;(record as any)[key] = change.to\n }\n record.updatedAt = new Date()\n },\n () =>\n parsed.isBase === true && record.isBase\n ? enforceBaseCurrency(em, record.id, record.organizationId, record.tenantId)\n : undefined,\n ],\n { transaction: true },\n )\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return { currencyId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadCurrencySnapshot(em, result.currencyId, ctx)\n },\n buildLog: async ({ snapshots, result }) => {\n const before = snapshots.before as CurrencySnapshot | undefined\n const after = snapshots.after as CurrencySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.update', 'Update currency'),\n resourceKind: 'currencies.currency',\n resourceId: after.id,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotBefore: before ?? undefined,\n snapshotAfter: after,\n payload: { undo: { before, after } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const before = payload?.before ?? null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(Currency, { id: before.id })\n if (!record) return\n Object.assign(record, {\n code: before.code,\n name: before.name,\n symbol: before.symbol,\n decimalPlaces: before.decimalPlaces,\n thousandsSeparator: before.thousandsSeparator,\n decimalSeparator: before.decimalSeparator,\n isBase: before.isBase,\n isActive: before.isActive,\n updatedAt: new Date(),\n })\n await em.flush()\n },\n}\n\nconst deleteCurrencyCommand: CommandHandler<CurrencyDeleteInput, { currencyId: string }> = {\n id: 'currencies.currencies.delete',\n async prepare(input, ctx) {\n requireId(input.id, 'Currency ID is required')\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadCurrencySnapshot(em, input.id, ctx)\n return { before }\n },\n async execute(input, ctx) {\n const parsed = currencyDeleteSchema.parse(input)\n requireId(parsed.id, 'Currency ID is required')\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!record) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, record)\n\n // Prevent deleting base currency\n if (record.isBase) {\n throw new CrudHttpError(400, { error: 'Cannot delete the base currency' })\n }\n\n // Prevent deleting currency with active exchange rates\n const activeRatesCount = await em.count(ExchangeRate, {\n $or: [\n { fromCurrencyCode: record.code },\n { toCurrencyCode: record.code },\n ],\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n deletedAt: null,\n isActive: true,\n })\n \n if (activeRatesCount > 0) {\n throw new CrudHttpError(400, { \n error: `Cannot delete currency ${record.code} because it has ${activeRatesCount} active exchange rate(s). Please delete or deactivate the exchange rates first.` \n })\n }\n\n record.deletedAt = new Date()\n record.isActive = false\n await em.flush()\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return { currencyId: record.id }\n },\n buildLog: async ({ snapshots, result }) => {\n const before = snapshots.before as CurrencySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.delete', 'Delete currency'),\n resourceKind: 'currencies.currency',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: { undo: { before } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const before = payload?.before ?? null\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await em.findOne(Currency, { id: before.id })\n if (!record) return\n record.deletedAt = null\n record.isActive = before.isActive\n record.updatedAt = new Date()\n await em.flush()\n },\n}\n\nregisterCommand(createCurrencyCommand)\nregisterCommand(updateCurrencyCommand)\nregisterCommand(deleteCurrencyCommand)\n\nexport { createCurrencyCommand, updateCurrencyCommand, deleteCurrencyCommand }\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { buildChanges, requireId, emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { resolveRedoSnapshot, restoreCreatedRow, reviveSnapshotSeed } from '@open-mercato/shared/lib/commands/redo'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, conflict, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { Currency, ExchangeRate } from '../data/entities'\nimport {\n currencyCreateSchema,\n currencyUpdateSchema,\n currencyDeleteSchema,\n type CurrencyCreateInput,\n type CurrencyUpdateInput,\n type CurrencyDeleteInput,\n} from '../data/validators'\nimport type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { buildCurrencyCommandWhere, ensureCurrencyCommandScope } from './scope'\n\nconst currencyCrudEvents: CrudEventsConfig = {\n module: 'currencies',\n entity: 'currency',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\ntype CurrencySnapshot = {\n id: string\n organizationId: string\n tenantId: string\n code: string\n name: string\n symbol: string | null\n decimalPlaces: number\n thousandsSeparator: string | null\n decimalSeparator: string | null\n isBase: boolean\n isActive: boolean\n createdAt: string\n updatedAt: string\n}\n\nasync function loadCurrencySnapshot(\n em: EntityManager,\n id: string,\n ctx: Parameters<typeof buildCurrencyCommandWhere>[0],\n): Promise<CurrencySnapshot | null> {\n const record = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id }),\n )\n if (!record) return null\n ensureCurrencyCommandScope(ctx, record)\n return {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n code: record.code,\n name: record.name,\n symbol: record.symbol ?? null,\n decimalPlaces: record.decimalPlaces,\n thousandsSeparator: record.thousandsSeparator ?? null,\n decimalSeparator: record.decimalSeparator ?? null,\n isBase: !!record.isBase,\n isActive: !!record.isActive,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt.toISOString(),\n }\n}\n\ntype CurrencyScope = {\n tenantId: string\n organizationId: string\n}\n\ntype BaseCurrencyTransition = {\n promoted: boolean\n demotedBaseCurrencyIds: string[]\n}\n\ntype CurrencyCommandResult = {\n currencyId: string\n base?: BaseCurrencyTransition\n}\n\ntype CurrencyUndoPayload = UndoPayload<CurrencySnapshot> & {\n base?: BaseCurrencyTransition\n}\n\nconst NO_BASE_TRANSITION: BaseCurrencyTransition = { promoted: false, demotedBaseCurrencyIds: [] }\n\nconst BASE_CURRENCY_LOCK_SQL = 'select pg_advisory_xact_lock(hashtextextended(?, 0))'\n\nconst UPDATABLE_CURRENCY_FIELDS = [\n 'code',\n 'name',\n 'symbol',\n 'decimalPlaces',\n 'thousandsSeparator',\n 'decimalSeparator',\n 'isBase',\n 'isActive',\n] as const\n\nfunction baseCurrencyLockKey(scope: CurrencyScope): string {\n return `currencies:base:${scope.tenantId}:${scope.organizationId}`\n}\n\n/**\n * Serialize every currency write inside one tenant and organization so the\n * \"exactly one live base currency\" invariant is decided on fresh rows. The\n * transaction owns the lock, so PostgreSQL releases it on commit/rollback.\n */\nasync function lockBaseCurrencyScope(em: EntityManager, scope: CurrencyScope): Promise<void> {\n if (!em.isInTransaction()) {\n throw new Error('[internal] Currency base lock requires an active transaction')\n }\n await em.execute(BASE_CURRENCY_LOCK_SQL, [baseCurrencyLockKey(scope)])\n}\n\nasync function findLiveBaseCurrencyIds(\n em: EntityManager,\n scope: CurrencyScope,\n excludeId?: string,\n): Promise<string[]> {\n const rows = await em.find(\n Currency,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isBase: true,\n deletedAt: null,\n ...(excludeId ? { id: { $ne: excludeId } } : {}),\n },\n { orderBy: { createdAt: 'asc', id: 'asc' } },\n )\n return rows.map((row) => row.id)\n}\n\nasync function setBaseFlag(\n em: EntityManager,\n scope: CurrencyScope,\n ids: string[],\n isBase: boolean,\n): Promise<void> {\n if (ids.length === 0) return\n await em.nativeUpdate(\n Currency,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n id: { $in: ids },\n },\n { isBase, updatedAt: new Date() },\n )\n}\n\nasync function findFirstLivePredecessorId(\n em: EntityManager,\n scope: CurrencyScope,\n candidateIds: string[],\n): Promise<string | null> {\n if (candidateIds.length === 0) return null\n const rows = await em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n id: { $in: candidateIds },\n deletedAt: null,\n })\n const liveIds = new Set(rows.map((row) => row.id))\n return candidateIds.find((id) => liveIds.has(id)) ?? null\n}\n\nasync function translatedHttpError(status: number, key: string, fallback: string): Promise<CrudHttpError> {\n const { translate } = await resolveTranslations()\n return new CrudHttpError(status, { error: translate(key, fallback) })\n}\n\nasync function resolvePredecessorOrRefuse(\n em: EntityManager,\n scope: CurrencyScope,\n candidateIds: string[],\n): Promise<string> {\n const predecessorId = await findFirstLivePredecessorId(em, scope, candidateIds)\n if (!predecessorId) {\n throw await translatedHttpError(\n 409,\n 'currencies.errors.undoWouldClearBase',\n \"This change can't be undone because it would leave the organization without a base currency.\",\n )\n }\n return predecessorId\n}\n\nfunction countActiveRatesForCode(em: EntityManager, scope: CurrencyScope, code: string): Promise<number> {\n return em.count(ExchangeRate, {\n $or: [\n { fromCurrencyCode: code },\n { toCurrencyCode: code },\n ],\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n isActive: true,\n })\n}\n\nfunction readScopedSnapshotRecord(em: EntityManager, snapshot: CurrencySnapshot): Promise<Currency | null> {\n return em.findOne(Currency, {\n id: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n })\n}\n\nfunction scopeOf(record: CurrencyScope): CurrencyScope {\n return { tenantId: record.tenantId, organizationId: record.organizationId }\n}\n\nfunction omitIsBase(input: CurrencyUpdateInput): Partial<CurrencyUpdateInput> {\n const rest: Partial<CurrencyUpdateInput> = { ...input }\n delete rest.isBase\n return rest\n}\n\nfunction isPromotionByLegacyLog(payload: CurrencyUndoPayload | null): boolean {\n return payload?.before?.isBase === false && payload?.after?.isBase === true\n}\n\nfunction isDemotionByLegacyLog(payload: CurrencyUndoPayload | null): boolean {\n return !payload?.base && payload?.before?.isBase === true && payload?.after?.isBase === false\n}\n\nconst createCurrencyCommand: CommandHandler<CurrencyCreateInput, CurrencyCommandResult> = {\n id: 'currencies.currencies.create',\n async execute(input, ctx) {\n const parsed = currencyCreateSchema.parse(input)\n ensureCurrencyCommandScope(ctx, parsed)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = scopeOf(parsed)\n let demotedBaseCurrencyIds: string[] = []\n let record!: Currency\n\n try {\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const existing = await em.findOne(Currency, {\n code: parsed.code,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n deletedAt: null,\n })\n if (existing) {\n throw conflict('Currency code already exists for this organization.')\n }\n if (parsed.isBase) {\n demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope)\n await setBaseFlag(em, scope, demotedBaseCurrencyIds, false)\n }\n },\n () => {\n const now = new Date()\n record = em.create(Currency, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n code: parsed.code,\n name: parsed.name,\n symbol: parsed.symbol ?? null,\n decimalPlaces: parsed.decimalPlaces ?? 2,\n thousandsSeparator: parsed.thousandsSeparator ?? null,\n decimalSeparator: parsed.decimalSeparator ?? null,\n isBase: parsed.isBase ?? false,\n isActive: parsed.isActive !== false,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n },\n ],\n { transaction: true },\n )\n } catch (err) {\n if (isUniqueViolation(err, 'currencies_code_scope_unique')) {\n throw conflict('Currency code already exists for this organization.')\n }\n throw err\n }\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return record.isBase\n ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } }\n : { currencyId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadCurrencySnapshot(em, result.currencyId, ctx)\n },\n buildLog: async ({ snapshots, result }) => {\n const after = snapshots.after as CurrencySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.create', 'Create currency'),\n resourceKind: 'currencies.currency',\n resourceId: after.id,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotAfter: after,\n payload: { undo: { after, base: result?.base ?? NO_BASE_TRANSITION } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const after = payload?.after ?? null\n if (!after) return\n const scope = scopeOf(after)\n const promoted = payload?.base ? payload.base.promoted : after.isBase\n const predecessorIds = payload?.base?.demotedBaseCurrencyIds ?? []\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let record: Currency | null = null\n let restoredBaseId: string | null = null\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await readScopedSnapshotRecord(em, after)\n if (!current || current.deletedAt) return\n const activeRatesCount = await countActiveRatesForCode(em, scope, current.code)\n if (activeRatesCount > 0) {\n throw await translatedHttpError(\n 409,\n 'currencies.errors.undoHasRates',\n \"This change can't be undone because exchange rates now use this currency. Delete or deactivate those exchange rates first.\",\n )\n }\n if (current.isBase) {\n restoredBaseId = await resolvePredecessorOrRefuse(em, scope, promoted ? predecessorIds : [])\n }\n record = current\n },\n () => {\n if (!record) return\n record.deletedAt = new Date()\n record.isActive = false\n record.isBase = false\n },\n () => (restoredBaseId ? setBaseFlag(em, scope, [restoredBaseId], true) : undefined),\n ],\n { transaction: true },\n )\n },\n redo: async ({ ctx, logEntry }) => {\n const snapshot = resolveRedoSnapshot<CurrencySnapshot>(logEntry)\n if (!snapshot || !snapshot.id) {\n throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for create command' })\n }\n const scope = scopeOf(snapshot)\n const isBase = snapshot.isBase === true\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let demotedBaseCurrencyIds: string[] = []\n let record!: Currency\n\n try {\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n if (isBase) {\n demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope, snapshot.id)\n await setBaseFlag(em, scope, demotedBaseCurrencyIds, false)\n }\n },\n async () => {\n record = await restoreCreatedRow(\n em,\n Currency,\n snapshot.id,\n () => reviveSnapshotSeed(snapshot as unknown as Record<string, unknown>),\n (lookupEm) => readScopedSnapshotRecord(lookupEm, snapshot),\n )\n record.isBase = isBase\n },\n ],\n { transaction: true },\n )\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw conflict('[internal] Cannot redo: a record with the same unique key already exists.')\n }\n throw err\n }\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return isBase\n ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } }\n : { currencyId: record.id }\n },\n}\n\nconst updateCurrencyCommand: CommandHandler<CurrencyUpdateInput, CurrencyCommandResult> = {\n id: 'currencies.currencies.update',\n async prepare(input, ctx) {\n requireId(input.id, 'Currency ID is required')\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadCurrencySnapshot(em, input.id, ctx)\n return { before }\n },\n async execute(input, ctx) {\n const parsed = currencyUpdateSchema.parse(input)\n requireId(parsed.id, 'Currency ID is required')\n\n const scopeRecord = await (ctx.container.resolve('em') as EntityManager).fork().findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!scopeRecord) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, scopeRecord)\n const scope = scopeOf(scopeRecord)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let record!: Currency\n let changes: Record<string, { from: unknown; to: unknown }> = {}\n let promoted = false\n let demotedBaseCurrencyIds: string[] = []\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!current) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, current)\n record = current\n\n if (parsed.code && parsed.code !== record.code) {\n const existing = await em.findOne(Currency, {\n code: parsed.code,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n id: { $ne: record.id },\n deletedAt: null,\n })\n if (existing) {\n throw conflict('Currency code already exists for this organization.')\n }\n }\n\n if (parsed.isBase === false && record.isBase) {\n throw await translatedHttpError(\n 400,\n 'currencies.errors.baseCurrencyUnset',\n 'Choose another currency as the base instead of clearing it.',\n )\n }\n\n const allChanges = buildChanges(\n record as unknown as Record<string, unknown>,\n parsed,\n UPDATABLE_CURRENCY_FIELDS,\n )\n changes = Object.fromEntries(\n Object.entries(allChanges).filter(([, change]) => change.to !== undefined),\n ) as Record<string, { from: unknown; to: unknown }>\n\n promoted = changes.isBase?.to === true\n if (promoted) {\n demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope, record.id)\n }\n },\n () => {\n if (Object.keys(changes).length === 0) return\n const assignments = Object.fromEntries(\n Object.entries(changes).map(([key, change]) => [key, change.to]),\n )\n Object.assign(record, assignments, { updatedAt: new Date() })\n },\n () => setBaseFlag(em, scope, demotedBaseCurrencyIds, false),\n ],\n { transaction: true },\n )\n\n const result: CurrencyCommandResult = promoted\n ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } }\n : { currencyId: record.id }\n if (Object.keys(changes).length === 0) {\n return result\n }\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return result\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadCurrencySnapshot(em, result.currencyId, ctx)\n },\n buildLog: async ({ input, snapshots, result }) => {\n const before = snapshots.before as CurrencySnapshot | undefined\n const after = snapshots.after as CurrencySnapshot | undefined\n if (!after) return null\n const { translate } = await resolveTranslations()\n const base = result?.base ?? NO_BASE_TRANSITION\n return {\n actionLabel: translate('currencies.audit.update', 'Update currency'),\n resourceKind: 'currencies.currency',\n resourceId: after.id,\n tenantId: after.tenantId,\n organizationId: after.organizationId,\n snapshotBefore: before ?? undefined,\n snapshotAfter: after,\n payload: { undo: { before, after, base } },\n redoInput: base.promoted ? input : omitIsBase(input),\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const before = payload?.before ?? null\n if (!before) return\n const scope = scopeOf(before)\n const promoted = payload?.base ? payload.base.promoted : isPromotionByLegacyLog(payload)\n const predecessorIds = payload?.base?.demotedBaseCurrencyIds ?? []\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const demotedByLegacyLog = isDemotionByLegacyLog(payload)\n let record: Currency | null = null\n let restoredBaseId: string | null = null\n let repromote = false\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await readScopedSnapshotRecord(em, before)\n if (!current) return\n if (promoted && current.isBase && !current.deletedAt) {\n restoredBaseId = await resolvePredecessorOrRefuse(em, scope, predecessorIds)\n }\n if (demotedByLegacyLog && !current.isBase && !current.deletedAt) {\n repromote = (await findLiveBaseCurrencyIds(em, scope)).length === 0\n }\n record = current\n },\n () => {\n if (!record) return\n Object.assign(record, {\n code: before.code,\n name: before.name,\n symbol: before.symbol,\n decimalPlaces: before.decimalPlaces,\n thousandsSeparator: before.thousandsSeparator,\n decimalSeparator: before.decimalSeparator,\n isActive: before.isActive,\n updatedAt: new Date(),\n })\n if (restoredBaseId) record.isBase = false\n if (repromote) record.isBase = true\n },\n () => (restoredBaseId ? setBaseFlag(em, scope, [restoredBaseId], true) : undefined),\n ],\n { transaction: true },\n )\n },\n}\n\nconst deleteCurrencyCommand: CommandHandler<CurrencyDeleteInput, { currencyId: string }> = {\n id: 'currencies.currencies.delete',\n async prepare(input, ctx) {\n requireId(input.id, 'Currency ID is required')\n const em = ctx.container.resolve('em') as EntityManager\n const before = await loadCurrencySnapshot(em, input.id, ctx)\n return { before }\n },\n async execute(input, ctx) {\n const parsed = currencyDeleteSchema.parse(input)\n requireId(parsed.id, 'Currency ID is required')\n\n const scopeRecord = await (ctx.container.resolve('em') as EntityManager).fork().findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!scopeRecord) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, scopeRecord)\n const scope = scopeOf(scopeRecord)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let record!: Currency\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await em.findOne(\n Currency,\n buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),\n )\n if (!current) {\n throw new CrudHttpError(404, { error: 'Currency not found' })\n }\n ensureCurrencyCommandScope(ctx, current)\n\n if (current.isBase) {\n throw new CrudHttpError(400, { error: 'Cannot delete the base currency' })\n }\n\n const activeRatesCount = await countActiveRatesForCode(em, scope, current.code)\n if (activeRatesCount > 0) {\n throw new CrudHttpError(400, {\n error: `Cannot delete currency ${current.code} because it has ${activeRatesCount} active exchange rate(s). Please delete or deactivate the exchange rates first.`,\n })\n }\n record = current\n },\n () => {\n record.deletedAt = new Date()\n record.isActive = false\n },\n ],\n { transaction: true },\n )\n\n const de = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: currencyCrudEvents,\n })\n\n return { currencyId: record.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as CurrencySnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('currencies.audit.delete', 'Delete currency'),\n resourceKind: 'currencies.currency',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: { undo: { before } },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<CurrencyUndoPayload>(logEntry)\n const before = payload?.before ?? null\n if (!before) return\n const scope = scopeOf(before)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let record: Currency | null = null\n let reviveAsNonBase = false\n\n await withAtomicFlush(\n em,\n [\n async () => {\n await lockBaseCurrencyScope(em, scope)\n const current = await readScopedSnapshotRecord(em, before)\n if (!current) return\n if (current.isBase) {\n const otherBaseIds = await findLiveBaseCurrencyIds(em, scope, current.id)\n reviveAsNonBase = otherBaseIds.length > 0\n }\n record = current\n },\n () => {\n if (!record) return\n record.deletedAt = null\n record.isActive = before.isActive\n if (reviveAsNonBase) record.isBase = false\n record.updatedAt = new Date()\n },\n ],\n { transaction: true },\n )\n },\n}\n\nregisterCommand(createCurrencyCommand)\nregisterCommand(updateCurrencyCommand)\nregisterCommand(deleteCurrencyCommand)\n\nexport { createCurrencyCommand, updateCurrencyCommand, deleteCurrencyCommand }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -110,6 +110,7 @@ async function seedExampleCurrencies(em, scope) {
|
|
|
110
110
|
for (const entry of existingEntries) {
|
|
111
111
|
existingMap.set(entry.code, entry);
|
|
112
112
|
}
|
|
113
|
+
const hasLiveBase = existingEntries.some((entry) => entry.isBase && !entry.deletedAt);
|
|
113
114
|
let touched = false;
|
|
114
115
|
for (const curr of SEED_CURRENCIES) {
|
|
115
116
|
const current = existingMap.get(curr.code);
|
|
@@ -126,6 +127,7 @@ async function seedExampleCurrencies(em, scope) {
|
|
|
126
127
|
tenantId: scope.tenantId,
|
|
127
128
|
organizationId: scope.organizationId,
|
|
128
129
|
...curr,
|
|
130
|
+
isBase: curr.isBase && !hasLiveBase,
|
|
129
131
|
createdAt: /* @__PURE__ */ new Date(),
|
|
130
132
|
updatedAt: /* @__PURE__ */ new Date()
|
|
131
133
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/currencies/lib/seeds.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Currency } from '../data/entities'\n\nexport type CurrencySeedScope = { tenantId: string; organizationId: string }\n\ntype CurrencySeed = {\n code: string\n name: string\n decimalPlaces: number\n symbol: string\n decimalSeparator: string\n thousandsSeparator: string\n isBase: boolean\n isActive: boolean\n}\n\nconst SEED_CURRENCIES: CurrencySeed[] = [\n {\n code: 'USD',\n name: 'US Dollar',\n decimalPlaces: 2,\n symbol: '$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: true,\n isActive: true,\n },\n {\n code: 'EUR',\n name: 'Euro',\n decimalPlaces: 2,\n symbol: '\u20AC',\n decimalSeparator: ',',\n thousandsSeparator: '.',\n isBase: false,\n isActive: true,\n },\n {\n code: 'JPY',\n name: 'Japanese Yen',\n decimalPlaces: 0,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'GBP',\n name: 'British Pound',\n decimalPlaces: 2,\n symbol: '\u00A3',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CHF',\n name: 'Swiss Franc',\n decimalPlaces: 2,\n symbol: 'Fr',\n decimalSeparator: '.',\n thousandsSeparator: \"'\",\n isBase: false,\n isActive: true,\n },\n {\n code: 'CAD',\n name: 'Canadian Dollar',\n decimalPlaces: 2,\n symbol: 'C$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'AUD',\n name: 'Australian Dollar',\n decimalPlaces: 2,\n symbol: 'A$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CNY',\n name: 'Chinese Yuan',\n decimalPlaces: 2,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CNH',\n name: 'Chinese Yuan (Offshore)',\n decimalPlaces: 2,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'PLN',\n name: 'Polish Zloty',\n decimalPlaces: 2,\n symbol: 'z\u0142',\n decimalSeparator: ',',\n thousandsSeparator: ' ',\n isBase: false,\n isActive: true,\n },\n]\n\nexport async function seedExampleCurrencies(em: EntityManager, scope: CurrencySeedScope): Promise<boolean> {\n const existingEntries = await em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const existingMap = new Map<string, Currency>()\n for (const entry of existingEntries) {\n existingMap.set(entry.code, entry)\n }\n\n let touched = false\n for (const curr of SEED_CURRENCIES) {\n const current = existingMap.get(curr.code)\n if (current) {\n if (current.name !== curr.name) {\n current.name = curr.name\n current.updatedAt = new Date()\n em.persist(current)\n touched = true\n }\n continue\n }\n const entry = em.create(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n ...curr,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n touched = true\n }\n\n if (touched) {\n await em.flush()\n }\n return touched\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,gBAAgB;AAezB,MAAM,kBAAkC;AAAA,EACtC;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AACF;AAEA,eAAsB,sBAAsB,IAAmB,OAA4C;AACzG,QAAM,kBAAkB,MAAM,GAAG,KAAK,UAAU;AAAA,IAC9C,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AACD,QAAM,cAAc,oBAAI,IAAsB;AAC9C,aAAW,SAAS,iBAAiB;AACnC,gBAAY,IAAI,MAAM,MAAM,KAAK;AAAA,EACnC;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Currency } from '../data/entities'\n\nexport type CurrencySeedScope = { tenantId: string; organizationId: string }\n\ntype CurrencySeed = {\n code: string\n name: string\n decimalPlaces: number\n symbol: string\n decimalSeparator: string\n thousandsSeparator: string\n isBase: boolean\n isActive: boolean\n}\n\nconst SEED_CURRENCIES: CurrencySeed[] = [\n {\n code: 'USD',\n name: 'US Dollar',\n decimalPlaces: 2,\n symbol: '$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: true,\n isActive: true,\n },\n {\n code: 'EUR',\n name: 'Euro',\n decimalPlaces: 2,\n symbol: '\u20AC',\n decimalSeparator: ',',\n thousandsSeparator: '.',\n isBase: false,\n isActive: true,\n },\n {\n code: 'JPY',\n name: 'Japanese Yen',\n decimalPlaces: 0,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'GBP',\n name: 'British Pound',\n decimalPlaces: 2,\n symbol: '\u00A3',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CHF',\n name: 'Swiss Franc',\n decimalPlaces: 2,\n symbol: 'Fr',\n decimalSeparator: '.',\n thousandsSeparator: \"'\",\n isBase: false,\n isActive: true,\n },\n {\n code: 'CAD',\n name: 'Canadian Dollar',\n decimalPlaces: 2,\n symbol: 'C$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'AUD',\n name: 'Australian Dollar',\n decimalPlaces: 2,\n symbol: 'A$',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CNY',\n name: 'Chinese Yuan',\n decimalPlaces: 2,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'CNH',\n name: 'Chinese Yuan (Offshore)',\n decimalPlaces: 2,\n symbol: '\u00A5',\n decimalSeparator: '.',\n thousandsSeparator: ',',\n isBase: false,\n isActive: true,\n },\n {\n code: 'PLN',\n name: 'Polish Zloty',\n decimalPlaces: 2,\n symbol: 'z\u0142',\n decimalSeparator: ',',\n thousandsSeparator: ' ',\n isBase: false,\n isActive: true,\n },\n]\n\nexport async function seedExampleCurrencies(em: EntityManager, scope: CurrencySeedScope): Promise<boolean> {\n const existingEntries = await em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const existingMap = new Map<string, Currency>()\n for (const entry of existingEntries) {\n existingMap.set(entry.code, entry)\n }\n const hasLiveBase = existingEntries.some((entry) => entry.isBase && !entry.deletedAt)\n\n let touched = false\n for (const curr of SEED_CURRENCIES) {\n const current = existingMap.get(curr.code)\n if (current) {\n if (current.name !== curr.name) {\n current.name = curr.name\n current.updatedAt = new Date()\n em.persist(current)\n touched = true\n }\n continue\n }\n const entry = em.create(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n ...curr,\n isBase: curr.isBase && !hasLiveBase,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(entry)\n touched = true\n }\n\n if (touched) {\n await em.flush()\n }\n return touched\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { loginSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
|
|
5
5
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
6
6
|
import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
|
|
7
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
7
8
|
import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
8
9
|
import { getClientIp } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
9
10
|
import {
|
|
@@ -18,6 +19,9 @@ import {
|
|
|
18
19
|
} from "@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext";
|
|
19
20
|
const metadata = { requireAuth: false };
|
|
20
21
|
const TIMING_EQUALIZATION_HASH = "$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i";
|
|
22
|
+
function reportLoginEventError(error) {
|
|
23
|
+
getTelemetryRuntime()?.reportError(error, { module: "customer_accounts", code: "customer_accounts.login_event_failed" });
|
|
24
|
+
}
|
|
21
25
|
async function POST(req) {
|
|
22
26
|
let body;
|
|
23
27
|
try {
|
|
@@ -56,23 +60,23 @@ async function POST(req) {
|
|
|
56
60
|
const user = await customerUserService.findByEmail(email, tenantId);
|
|
57
61
|
if (!user || !user.passwordHash) {
|
|
58
62
|
await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
|
|
59
|
-
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_credentials", tenantId }).catch(
|
|
63
|
+
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_credentials", tenantId }).catch(reportLoginEventError);
|
|
60
64
|
return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
|
|
61
65
|
}
|
|
62
66
|
if (!user.isActive) {
|
|
63
67
|
await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
|
|
64
|
-
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "inactive", tenantId }).catch(
|
|
68
|
+
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "inactive", tenantId }).catch(reportLoginEventError);
|
|
65
69
|
return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
|
|
66
70
|
}
|
|
67
71
|
if (customerUserService.checkLockout(user)) {
|
|
68
72
|
await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
|
|
69
|
-
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(
|
|
73
|
+
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(reportLoginEventError);
|
|
70
74
|
return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
|
|
71
75
|
}
|
|
72
76
|
const passwordValid = await customerUserService.verifyPassword(user, password);
|
|
73
77
|
if (!passwordValid) {
|
|
74
78
|
await customerUserService.incrementFailedAttempts(user);
|
|
75
|
-
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_password", tenantId }).catch(
|
|
79
|
+
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_password", tenantId }).catch(reportLoginEventError);
|
|
76
80
|
return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
|
|
77
81
|
}
|
|
78
82
|
if (!user.emailVerifiedAt) {
|
|
@@ -80,10 +84,13 @@ async function POST(req) {
|
|
|
80
84
|
email,
|
|
81
85
|
reason: "email_not_verified",
|
|
82
86
|
tenantId
|
|
83
|
-
}).catch(
|
|
87
|
+
}).catch(reportLoginEventError);
|
|
88
|
+
return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
|
|
89
|
+
}
|
|
90
|
+
if (!await customerUserService.resetFailedAttemptsUnlessLocked(user)) {
|
|
91
|
+
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "locked", tenantId }).catch(reportLoginEventError);
|
|
84
92
|
return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
|
|
85
93
|
}
|
|
86
|
-
await customerUserService.resetFailedAttempts(user);
|
|
87
94
|
await customerUserService.updateLastLoginAt(user);
|
|
88
95
|
if (compoundKey) {
|
|
89
96
|
await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig);
|
|
@@ -100,7 +107,7 @@ async function POST(req) {
|
|
|
100
107
|
email: user.email,
|
|
101
108
|
tenantId,
|
|
102
109
|
organizationId: user.organizationId
|
|
103
|
-
}).catch(
|
|
110
|
+
}).catch(reportLoginEventError);
|
|
104
111
|
const res = NextResponse.json({
|
|
105
112
|
ok: true,
|
|
106
113
|
user: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/api/login.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the real-account and unknown/inactive/locked/no-hash login branches\n// so account existence cannot be inferred from a timing side channel. Mirrors signup.ts.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n\n // Equalize response timing and error responses across every failed-login branch so an\n // attacker cannot enumerate which emails have an account in this tenant. Unknown,\n // password-less, inactive, locked, wrong-password, and unverified accounts all run a\n // bcrypt comparison and return the same generic 401 \u2014 lockout/deactivation guidance is\n // conveyed out-of-band (e.g. email), never in the synchronous response.\n if (!user || !user.passwordHash) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,WAAW,qBAAqB;AACzC,SAAS,SAAS;AAElB,SAAS,mBAAmB;AAC5B,SAAS,8BAA8B;AAIvC,SAAS,iCAAiC;AAC1C,SAAS,4BAA4B;AACrC,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAqD,EAAE,aAAa,MAAM;AAKvF,MAAM,2BAA2B;AAEjC,eAAsB,KAAK,KAAc;AACvC,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxF;AAEA,QAAM,SAAS,YAAY,UAAU,IAAI;AACzC,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,sBAAsB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvF;AAEA,QAAM,EAAE,OAAO,SAAS,IAAI,OAAO;AACnC,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,MAAM,qBAAqB,KAAK,OAAO,KAAK,UAAU;AAAA,MACpE,gBAAgB,OAAO,KAAK,kBAAkB;AAAA,IAChD,CAAC;AACD,eAAW,QAAQ;AAAA,EACrB,SAAS,KAAK;AACZ,QAAI,eAAe,uBAAuB;AACxC,aAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IACpF;AACA,UAAM;AAAA,EACR;AAEA,QAAM,EAAE,OAAO,gBAAgB,YAAY,IAAI,MAAM,mBAAmB;AAAA,IACtE;AAAA,IACA,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,yBAAyB,UAAU,QAAQ,wBAAwB;AACzE,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AAEnE,QAAM,OAAO,MAAM,oBAAoB,YAAY,OAAO,QAAQ;AAOlE,MAAI,CAAC,QAAQ,CAAC,KAAK,cAAc;AAC/B,UAAM,cAAc,UAAU,wBAAwB;AACtD,SAAK,0BAA0B,kCAAkC,EAAE,OAAO,QAAQ,uBAAuB,SAAS,CAAC,EAAE,MAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the real-account and unknown/inactive/locked/no-hash login branches\n// so account existence cannot be inferred from a timing side channel. Mirrors signup.ts.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\n\nfunction reportLoginEventError(error: unknown): void {\n getTelemetryRuntime()?.reportError(error, { module: 'customer_accounts', code: 'customer_accounts.login_event_failed' })\n}\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n\n // Equalize response timing and error responses across every failed-login branch so an\n // attacker cannot enumerate which emails have an account in this tenant. Unknown,\n // password-less, inactive, locked, wrong-password, and unverified accounts all run a\n // bcrypt comparison and return the same generic 401 \u2014 lockout/deactivation guidance is\n // conveyed out-of-band (e.g. email), never in the synchronous response.\n if (!user || !user.passwordHash) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'inactive', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!(await customerUserService.resetFailedAttemptsUnlessLocked(user))) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(reportLoginEventError)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId,\n organizationId: user.organizationId,\n })\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId: user.organizationId,\n }).catch(reportLoginEventError)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate customer credentials',\n description: 'Validates customer credentials and issues JWT + session cookies.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: loginSchema,\n description: 'Login payload with email and password.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Invalid credentials', schema: errorSchema },\n { status: 429, description: 'Too many login attempts', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer login',\n description: 'Handles customer authentication and session issuance.',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -64,22 +64,22 @@ class CustomerUserService {
|
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
66
|
async incrementFailedAttempts(user) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
const lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS);
|
|
68
|
+
await this.em.getKysely().updateTable("customer_users").set((eb) => ({
|
|
69
|
+
failed_login_attempts: eb("failed_login_attempts", "+", 1),
|
|
70
|
+
locked_until: eb.case().when("failed_login_attempts", ">=", MAX_FAILED_ATTEMPTS - 1).then(eb.fn("greatest", [eb.ref("locked_until"), eb.val(lockedUntil)])).else(eb.ref("locked_until")).end()
|
|
71
|
+
})).where("id", "=", user.id).execute();
|
|
72
|
+
}
|
|
73
|
+
async resetFailedAttemptsUnlessLocked(user) {
|
|
74
|
+
const now = /* @__PURE__ */ new Date();
|
|
75
|
+
const row = await this.em.getKysely().updateTable("customer_users").set({ failed_login_attempts: 0, locked_until: null }).where("id", "=", user.id).where((eb) => eb.or([eb("locked_until", "is", null), eb("locked_until", "<=", now)])).returning("id").executeTakeFirst();
|
|
76
|
+
return row !== void 0;
|
|
75
77
|
}
|
|
76
78
|
async resetFailedAttempts(user) {
|
|
77
79
|
await this.em.nativeUpdate(CustomerUser, { id: user.id }, {
|
|
78
80
|
failedLoginAttempts: 0,
|
|
79
81
|
lockedUntil: null
|
|
80
82
|
});
|
|
81
|
-
user.failedLoginAttempts = 0;
|
|
82
|
-
user.lockedUntil = null;
|
|
83
83
|
}
|
|
84
84
|
async updatePassword(user, newPassword, em) {
|
|
85
85
|
const passwordHash = await hash(newPassword, BCRYPT_COST);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/services/customerUserService.ts"],
|
|
4
|
-
"sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { hash, compare } from 'bcryptjs'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { hashForLookup, lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\nconst MAX_FAILED_ATTEMPTS = 5\nconst LOCKOUT_DURATION_MS = 15 * 60 * 1000 // 15 minutes\n\nexport class CustomerUserService {\n constructor(private em: EntityManager) {}\n\n async createUser(\n email: string,\n password: string,\n displayName: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<CustomerUser> {\n const passwordHash = await hash(password, BCRYPT_COST)\n const emailHash = hashForLookup(email)\n const user = this.em.create(CustomerUser, {\n email: email.toLowerCase().trim(),\n emailHash,\n passwordHash,\n displayName,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n failedLoginAttempts: 0,\n createdAt: new Date(),\n } as any)\n return user as CustomerUser\n }\n\n async findByEmail(email: string, tenantId: string): Promise<CustomerUser | null> {\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n {\n emailHash: { $in: lookupHashCandidates(email) },\n tenantId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId },\n )\n }\n\n async findById(\n id: string,\n tenantId: string,\n organizationId?: string | null,\n ): Promise<CustomerUser | null> {\n const where: Record<string, unknown> = { id, tenantId, deletedAt: null }\n if (organizationId !== undefined) where.organizationId = organizationId\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n where as any,\n undefined,\n { tenantId, organizationId },\n )\n }\n\n async verifyPassword(user: CustomerUser, password: string): Promise<boolean> {\n if (!user.passwordHash) return false\n return compare(password, user.passwordHash)\n }\n\n async updateLastLoginAt(user: CustomerUser): Promise<void> {\n const now = new Date()\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, { lastLoginAt: now })\n user.lastLoginAt = now\n }\n\n checkLockout(user: CustomerUser): boolean {\n if (!user.lockedUntil) return false\n if (user.lockedUntil.getTime() > Date.now()) return true\n return false\n }\n\n async incrementFailedAttempts(user: CustomerUser): Promise<void> {\n const
|
|
5
|
-
"mappings": "AACA,SAAS,MAAM,eAAe;AAC9B,SAAS,oBAAoB;AAC7B,SAAS,eAAe,4BAA4B;AACpD,SAAS,6BAA6B;AAEtC,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAC5B,MAAM,sBAAsB,KAAK,KAAK;
|
|
4
|
+
"sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { hash, compare } from 'bcryptjs'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { hashForLookup, lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\nconst MAX_FAILED_ATTEMPTS = 5\nconst LOCKOUT_DURATION_MS = 15 * 60 * 1000 // 15 minutes\n\ntype CustomerUserLockoutDatabase = {\n customer_users: {\n id: string\n failed_login_attempts: number\n locked_until: Date | null\n }\n}\n\nexport class CustomerUserService {\n constructor(private em: EntityManager) {}\n\n async createUser(\n email: string,\n password: string,\n displayName: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<CustomerUser> {\n const passwordHash = await hash(password, BCRYPT_COST)\n const emailHash = hashForLookup(email)\n const user = this.em.create(CustomerUser, {\n email: email.toLowerCase().trim(),\n emailHash,\n passwordHash,\n displayName,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n failedLoginAttempts: 0,\n createdAt: new Date(),\n } as any)\n return user as CustomerUser\n }\n\n async findByEmail(email: string, tenantId: string): Promise<CustomerUser | null> {\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n {\n emailHash: { $in: lookupHashCandidates(email) },\n tenantId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId },\n )\n }\n\n async findById(\n id: string,\n tenantId: string,\n organizationId?: string | null,\n ): Promise<CustomerUser | null> {\n const where: Record<string, unknown> = { id, tenantId, deletedAt: null }\n if (organizationId !== undefined) where.organizationId = organizationId\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n where as any,\n undefined,\n { tenantId, organizationId },\n )\n }\n\n async verifyPassword(user: CustomerUser, password: string): Promise<boolean> {\n if (!user.passwordHash) return false\n return compare(password, user.passwordHash)\n }\n\n async updateLastLoginAt(user: CustomerUser): Promise<void> {\n const now = new Date()\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, { lastLoginAt: now })\n user.lastLoginAt = now\n }\n\n checkLockout(user: CustomerUser): boolean {\n if (!user.lockedUntil) return false\n if (user.lockedUntil.getTime() > Date.now()) return true\n return false\n }\n\n async incrementFailedAttempts(user: CustomerUser): Promise<void> {\n const lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS)\n await this.em.getKysely<CustomerUserLockoutDatabase>()\n .updateTable('customer_users')\n .set((eb) => ({\n failed_login_attempts: eb('failed_login_attempts', '+', 1),\n locked_until: eb\n .case()\n .when('failed_login_attempts', '>=', MAX_FAILED_ATTEMPTS - 1)\n .then(eb.fn<Date>('greatest', [eb.ref('locked_until'), eb.val(lockedUntil)]))\n .else(eb.ref('locked_until'))\n .end(),\n }))\n .where('id', '=', user.id)\n .execute()\n }\n\n async resetFailedAttemptsUnlessLocked(user: CustomerUser): Promise<boolean> {\n const now = new Date()\n const row = await this.em.getKysely<CustomerUserLockoutDatabase>()\n .updateTable('customer_users')\n .set({ failed_login_attempts: 0, locked_until: null })\n .where('id', '=', user.id)\n .where((eb) => eb.or([eb('locked_until', 'is', null), eb('locked_until', '<=', now)]))\n .returning('id')\n .executeTakeFirst()\n return row !== undefined\n }\n\n async resetFailedAttempts(user: CustomerUser): Promise<void> {\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, {\n failedLoginAttempts: 0,\n lockedUntil: null,\n })\n }\n\n async updatePassword(user: CustomerUser, newPassword: string, em?: EntityManager): Promise<void> {\n const passwordHash = await hash(newPassword, BCRYPT_COST)\n await (em ?? this.em).nativeUpdate(CustomerUser, {\n id: user.id,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n }, { passwordHash })\n user.passwordHash = passwordHash\n }\n\n // `display_name` is encrypted at rest. `nativeUpdate` issues raw SQL and fires none of the\n // flush hooks the tenant-encryption subscriber depends on, so writing it that way persists\n // plaintext PII into a ciphertext column (#3837). Assign it on the managed entity and flush\n // so `beforeUpdate` encrypts the value on its way to the database.\n async updateProfile(user: CustomerUser, data: { displayName?: string }): Promise<void> {\n if (data.displayName === undefined) return\n const managed = await findOneWithDecryption(\n this.em,\n CustomerUser,\n { id: user.id, tenantId: user.tenantId, organizationId: user.organizationId, deletedAt: null } as any,\n undefined,\n { tenantId: user.tenantId, organizationId: user.organizationId },\n )\n if (!managed) return\n managed.displayName = data.displayName\n await this.em.flush()\n user.displayName = data.displayName\n }\n\n async softDelete(\n userId: string,\n scope?: { tenantId: string; organizationId: string | null },\n ): Promise<void> {\n const where: Record<string, unknown> = { id: userId }\n if (scope) {\n where.tenantId = scope.tenantId\n where.organizationId = scope.organizationId\n }\n await this.em.nativeUpdate(CustomerUser, where, {\n deletedAt: new Date(),\n isActive: false,\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
CustomerPersonProfile,
|
|
23
23
|
CustomerTagAssignment
|
|
24
24
|
} from "../data/entities.js";
|
|
25
|
+
import {
|
|
26
|
+
captureInteractionRestoreDetails,
|
|
27
|
+
omitInteractionRestoreDetails,
|
|
28
|
+
restoreInteractionRestoreDetails
|
|
29
|
+
} from "./interactionRestoreDetails.js";
|
|
25
30
|
import {
|
|
26
31
|
companyCreateSchema,
|
|
27
32
|
companyUpdateSchema
|
|
@@ -98,7 +103,7 @@ function companyEntityIndexEntry(entity) {
|
|
|
98
103
|
organizationId: entity.organizationId
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
|
-
async function loadCompanySnapshot(em, id) {
|
|
106
|
+
async function loadCompanySnapshot(em, id, options = {}) {
|
|
102
107
|
const entity = await em.findOne(CustomerEntity, { id, deletedAt: null });
|
|
103
108
|
if (!entity || entity.kind !== "company") return null;
|
|
104
109
|
const profile = await em.findOne(CustomerCompanyProfile, { entity });
|
|
@@ -248,6 +253,7 @@ async function loadCompanySnapshot(em, id) {
|
|
|
248
253
|
createdAt: interaction.createdAt,
|
|
249
254
|
updatedAt: interaction.updatedAt,
|
|
250
255
|
deletedAt: interaction.deletedAt ?? null,
|
|
256
|
+
...options.includeInteractionRestoreDetails ? captureInteractionRestoreDetails(interaction) : {},
|
|
251
257
|
custom: await loadCustomFieldSnapshot(em, {
|
|
252
258
|
entityId: INTERACTION_ENTITY_ID,
|
|
253
259
|
recordId: interaction.id,
|
|
@@ -792,7 +798,7 @@ const deleteCompanyCommand = {
|
|
|
792
798
|
async prepare(input, ctx) {
|
|
793
799
|
const id = requireId(input, "Company id required");
|
|
794
800
|
const em = ctx.container.resolve("em");
|
|
795
|
-
const snapshot = await loadCompanySnapshot(em, id);
|
|
801
|
+
const snapshot = await loadCompanySnapshot(em, id, { includeInteractionRestoreDetails: true });
|
|
796
802
|
return snapshot ? { before: snapshot } : {};
|
|
797
803
|
},
|
|
798
804
|
async execute(input, ctx) {
|
|
@@ -966,7 +972,7 @@ const deleteCompanyCommand = {
|
|
|
966
972
|
resourceId: before.entity.id,
|
|
967
973
|
tenantId: before.entity.tenantId,
|
|
968
974
|
organizationId: before.entity.organizationId,
|
|
969
|
-
snapshotBefore: before,
|
|
975
|
+
snapshotBefore: omitInteractionRestoreDetails(before),
|
|
970
976
|
payload: {
|
|
971
977
|
undo: {
|
|
972
978
|
before
|
|
@@ -1193,6 +1199,7 @@ const deleteCompanyCommand = {
|
|
|
1193
1199
|
source: interaction.source,
|
|
1194
1200
|
appearanceIcon: interaction.appearanceIcon,
|
|
1195
1201
|
appearanceColor: interaction.appearanceColor,
|
|
1202
|
+
...restoreInteractionRestoreDetails(interaction),
|
|
1196
1203
|
createdAt: interaction.createdAt,
|
|
1197
1204
|
updatedAt: interaction.updatedAt,
|
|
1198
1205
|
deletedAt: interaction.deletedAt
|