@open-mercato/enterprise 0.7.1-develop.7130.1.fef2396fd8 → 0.7.1-develop.7131.1.20abf9e885
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/dist/modules/security/api/enforcement/_shared.js +5 -0
- package/dist/modules/security/api/enforcement/_shared.js.map +2 -2
- package/dist/modules/security/api/mfa/_shared.js +5 -0
- package/dist/modules/security/api/mfa/_shared.js.map +2 -2
- package/dist/modules/security/api/profile/password/route.js +5 -0
- package/dist/modules/security/api/profile/password/route.js.map +2 -2
- package/dist/modules/security/api/sudo/_shared.js +5 -0
- package/dist/modules/security/api/sudo/_shared.js.map +2 -2
- package/dist/modules/security/api/users/_shared.js +5 -0
- package/dist/modules/security/api/users/_shared.js.map +2 -2
- package/jest.config.cjs +2 -0
- package/package.json +5 -5
- package/src/modules/security/api/enforcement/_shared.ts +5 -0
- package/src/modules/security/api/mfa/_shared.ts +5 -0
- package/src/modules/security/api/profile/password/route.ts +5 -0
- package/src/modules/security/api/sudo/_shared.ts +5 -0
- package/src/modules/security/api/users/_shared.ts +5 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { Organization, Tenant } from "@open-mercato/core/modules/directory/data/entities";
|
|
3
3
|
import { CrudHttpError, forbidden } from "@open-mercato/shared/lib/crud/errors";
|
|
4
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
4
5
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
6
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
6
7
|
import { enforceTenantSelection, resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
|
|
@@ -90,6 +91,10 @@ async function mapEnforcementError(error) {
|
|
|
90
91
|
if (error instanceof CrudHttpError) {
|
|
91
92
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status });
|
|
92
93
|
}
|
|
94
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error);
|
|
95
|
+
if (interceptorRejection) {
|
|
96
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
97
|
+
}
|
|
93
98
|
if (isMfaEnforcementServiceError(error)) {
|
|
94
99
|
return securityApiError(error.statusCode, error.message);
|
|
95
100
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/security/api/enforcement/_shared.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { CrudHttpError, forbidden } 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 { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { EnforcementScope, type MfaEnforcementPolicy } from '../../data/entities'\nimport type {\n EnforcementActorContext,\n MfaEnforcementServiceError,\n MfaEnforcementService,\n} from '../../services/MfaEnforcementService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'enforcement' })\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type EnforcementRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n enforcementService: MfaEnforcementService\n}\n\nfunction normalizeNullableString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction normalizeOrganizationList(values: unknown): string[] | null {\n if (values === null || values === undefined) return null\n if (!Array.isArray(values)) return null\n const result: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (trimmed) result.push(trimmed)\n }\n return result\n}\n\nexport async function resolveActorContext(ctx: EnforcementRequestContext): Promise<EnforcementActorContext> {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n return {\n tenantId: normalizeNullableString(ctx.auth.tenantId),\n isSuperAdmin,\n }\n}\n\nasync function assertActorOwnsOrganization(\n ctx: EnforcementRequestContext,\n organizationId: string,\n): Promise<void> {\n const rbacService = ctx.container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(ctx.auth.sub, {\n tenantId: normalizeNullableString(ctx.auth.tenantId),\n organizationId: normalizeNullableString(ctx.auth.orgId),\n })\n const organizations = normalizeOrganizationList(acl?.organizations)\n if (organizations === null || organizations.includes('__all__')) return\n if (!organizations.includes(organizationId)) {\n throw forbidden('Not authorized to target this organization.')\n }\n}\n\nexport async function assertActorOwnsEnforcementScope(\n ctx: EnforcementRequestContext,\n scope: EnforcementScope,\n scopeId: string | null | undefined,\n): Promise<void> {\n if (scope === EnforcementScope.PLATFORM) {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (!isSuperAdmin) {\n throw forbidden('Platform scope requires platform administrator privileges.')\n }\n return\n }\n\n if (scope === EnforcementScope.TENANT) {\n await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, scopeId)\n return\n }\n\n const normalizedScopeId = normalizeNullableString(scopeId)\n if (!normalizedScopeId) {\n throw new CrudHttpError(400, { error: \"organisation scopeId must use '<tenantId>:<organizationId>' format\" })\n }\n const [tenantId, organizationId] = normalizedScopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new CrudHttpError(400, { error: \"organisation scopeId must use '<tenantId>:<organizationId>' format\" })\n }\n\n await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, tenantId)\n\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (isSuperAdmin) return\n await assertActorOwnsOrganization(ctx, organizationId)\n}\n\nexport async function resolveEnforcementContext(req: Request): Promise<EnforcementRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n enforcementService: container.resolve<MfaEnforcementService>('mfaEnforcementService'),\n }\n}\n\nexport async function mapEnforcementError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isMfaEnforcementServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n logger.error('Enforcement route failure', { err: error })\n return securityApiError(500, 'Failed to process enforcement request.')\n}\n\nexport function toPolicyResponse(policy: MfaEnforcementPolicy): {\n id: string\n scope: string\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: string | null\n enforcedBy: string\n createdAt: string\n updatedAt: string\n} {\n return {\n id: policy.id,\n scope: policy.scope,\n tenantId: policy.tenantId ?? null,\n tenantName: null,\n organizationId: policy.organizationId ?? null,\n organizationName: null,\n isEnforced: policy.isEnforced,\n allowedMethods: policy.allowedMethods ?? null,\n enforcementDeadline: policy.enforcementDeadline ? policy.enforcementDeadline.toISOString() : null,\n enforcedBy: policy.enforcedBy,\n createdAt: policy.createdAt.toISOString(),\n updatedAt: policy.updatedAt.toISOString(),\n }\n}\n\nexport async function attachPolicyScopeNames(\n container: RequestContainer,\n policies: MfaEnforcementPolicy[],\n): Promise<Array<ReturnType<typeof toPolicyResponse>>> {\n if (policies.length === 0) return []\n\n const em = container.resolve<EntityManager>('em')\n const tenantIds = Array.from(\n new Set(\n policies\n .map((policy) => policy.tenantId ?? null)\n .filter((tenantId): tenantId is string => typeof tenantId === 'string' && tenantId.length > 0),\n ),\n )\n const organizationIds = Array.from(\n new Set(\n policies\n .map((policy) => policy.organizationId ?? null)\n .filter((organizationId): organizationId is string => typeof organizationId === 'string' && organizationId.length > 0),\n ),\n )\n\n const [tenants, organizations] = await Promise.all([\n tenantIds.length\n ? em.find(Tenant, { id: { $in: tenantIds }, deletedAt: null })\n : Promise.resolve([]),\n organizationIds.length\n ? em.find(Organization, { id: { $in: organizationIds }, deletedAt: null })\n : Promise.resolve([]),\n ])\n\n const tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tenantId = tenant?.id ? String(tenant.id) : null\n if (!tenantId) return acc\n acc[tenantId] = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : tenantId\n return acc\n }, {})\n const organizationMap = organizations.reduce<Record<string, string>>((acc, organization) => {\n const organizationId = organization?.id ? String(organization.id) : null\n if (!organizationId) return acc\n acc[organizationId] = typeof organization.name === 'string' && organization.name.length > 0\n ? organization.name\n : organizationId\n return acc\n }, {})\n\n return policies.map((policy) => {\n const response = toPolicyResponse(policy)\n return {\n ...response,\n tenantName: response.tenantId ? tenantMap[response.tenantId] ?? response.tenantId : null,\n organizationName: response.organizationId\n ? organizationMap[response.organizationId] ?? response.organizationId\n : null,\n }\n })\n}\n\nfunction isMfaEnforcementServiceError(error: unknown): error is MfaEnforcementServiceError {\n return error instanceof Error\n && error.name === 'MfaEnforcementServiceError'\n && typeof (error as Partial<MfaEnforcementServiceError>).statusCode === 'number'\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { EnforcementScope, type MfaEnforcementPolicy } from '../../data/entities'\nimport type {\n EnforcementActorContext,\n MfaEnforcementServiceError,\n MfaEnforcementService,\n} from '../../services/MfaEnforcementService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'enforcement' })\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type EnforcementRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n enforcementService: MfaEnforcementService\n}\n\nfunction normalizeNullableString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction normalizeOrganizationList(values: unknown): string[] | null {\n if (values === null || values === undefined) return null\n if (!Array.isArray(values)) return null\n const result: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (trimmed) result.push(trimmed)\n }\n return result\n}\n\nexport async function resolveActorContext(ctx: EnforcementRequestContext): Promise<EnforcementActorContext> {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n return {\n tenantId: normalizeNullableString(ctx.auth.tenantId),\n isSuperAdmin,\n }\n}\n\nasync function assertActorOwnsOrganization(\n ctx: EnforcementRequestContext,\n organizationId: string,\n): Promise<void> {\n const rbacService = ctx.container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(ctx.auth.sub, {\n tenantId: normalizeNullableString(ctx.auth.tenantId),\n organizationId: normalizeNullableString(ctx.auth.orgId),\n })\n const organizations = normalizeOrganizationList(acl?.organizations)\n if (organizations === null || organizations.includes('__all__')) return\n if (!organizations.includes(organizationId)) {\n throw forbidden('Not authorized to target this organization.')\n }\n}\n\nexport async function assertActorOwnsEnforcementScope(\n ctx: EnforcementRequestContext,\n scope: EnforcementScope,\n scopeId: string | null | undefined,\n): Promise<void> {\n if (scope === EnforcementScope.PLATFORM) {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (!isSuperAdmin) {\n throw forbidden('Platform scope requires platform administrator privileges.')\n }\n return\n }\n\n if (scope === EnforcementScope.TENANT) {\n await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, scopeId)\n return\n }\n\n const normalizedScopeId = normalizeNullableString(scopeId)\n if (!normalizedScopeId) {\n throw new CrudHttpError(400, { error: \"organisation scopeId must use '<tenantId>:<organizationId>' format\" })\n }\n const [tenantId, organizationId] = normalizedScopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new CrudHttpError(400, { error: \"organisation scopeId must use '<tenantId>:<organizationId>' format\" })\n }\n\n await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, tenantId)\n\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (isSuperAdmin) return\n await assertActorOwnsOrganization(ctx, organizationId)\n}\n\nexport async function resolveEnforcementContext(req: Request): Promise<EnforcementRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n enforcementService: container.resolve<MfaEnforcementService>('mfaEnforcementService'),\n }\n}\n\nexport async function mapEnforcementError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (isMfaEnforcementServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n logger.error('Enforcement route failure', { err: error })\n return securityApiError(500, 'Failed to process enforcement request.')\n}\n\nexport function toPolicyResponse(policy: MfaEnforcementPolicy): {\n id: string\n scope: string\n tenantId: string | null\n tenantName: string | null\n organizationId: string | null\n organizationName: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: string | null\n enforcedBy: string\n createdAt: string\n updatedAt: string\n} {\n return {\n id: policy.id,\n scope: policy.scope,\n tenantId: policy.tenantId ?? null,\n tenantName: null,\n organizationId: policy.organizationId ?? null,\n organizationName: null,\n isEnforced: policy.isEnforced,\n allowedMethods: policy.allowedMethods ?? null,\n enforcementDeadline: policy.enforcementDeadline ? policy.enforcementDeadline.toISOString() : null,\n enforcedBy: policy.enforcedBy,\n createdAt: policy.createdAt.toISOString(),\n updatedAt: policy.updatedAt.toISOString(),\n }\n}\n\nexport async function attachPolicyScopeNames(\n container: RequestContainer,\n policies: MfaEnforcementPolicy[],\n): Promise<Array<ReturnType<typeof toPolicyResponse>>> {\n if (policies.length === 0) return []\n\n const em = container.resolve<EntityManager>('em')\n const tenantIds = Array.from(\n new Set(\n policies\n .map((policy) => policy.tenantId ?? null)\n .filter((tenantId): tenantId is string => typeof tenantId === 'string' && tenantId.length > 0),\n ),\n )\n const organizationIds = Array.from(\n new Set(\n policies\n .map((policy) => policy.organizationId ?? null)\n .filter((organizationId): organizationId is string => typeof organizationId === 'string' && organizationId.length > 0),\n ),\n )\n\n const [tenants, organizations] = await Promise.all([\n tenantIds.length\n ? em.find(Tenant, { id: { $in: tenantIds }, deletedAt: null })\n : Promise.resolve([]),\n organizationIds.length\n ? em.find(Organization, { id: { $in: organizationIds }, deletedAt: null })\n : Promise.resolve([]),\n ])\n\n const tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const tenantId = tenant?.id ? String(tenant.id) : null\n if (!tenantId) return acc\n acc[tenantId] = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : tenantId\n return acc\n }, {})\n const organizationMap = organizations.reduce<Record<string, string>>((acc, organization) => {\n const organizationId = organization?.id ? String(organization.id) : null\n if (!organizationId) return acc\n acc[organizationId] = typeof organization.name === 'string' && organization.name.length > 0\n ? organization.name\n : organizationId\n return acc\n }, {})\n\n return policies.map((policy) => {\n const response = toPolicyResponse(policy)\n return {\n ...response,\n tenantName: response.tenantId ? tenantMap[response.tenantId] ?? response.tenantId : null,\n organizationName: response.organizationId\n ? organizationMap[response.organizationId] ?? response.organizationId\n : null,\n }\n })\n}\n\nfunction isMfaEnforcementServiceError(error: unknown): error is MfaEnforcementServiceError {\n return error instanceof Error\n && error.name === 'MfaEnforcementServiceError'\n && typeof (error as Partial<MfaEnforcementServiceError>).statusCode === 'number'\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["isSuperAdmin"]
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
4
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
4
5
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
6
|
import "../../lib/mfaCompletionRoutes.js";
|
|
6
7
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
@@ -76,6 +77,10 @@ async function mapMfaError(error) {
|
|
|
76
77
|
if (error instanceof CrudHttpError) {
|
|
77
78
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status });
|
|
78
79
|
}
|
|
80
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error);
|
|
81
|
+
if (interceptorRejection) {
|
|
82
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
83
|
+
}
|
|
79
84
|
if (isMfaServiceError(error) || isMfaVerificationServiceError(error)) {
|
|
80
85
|
return securityApiError(error.statusCode, error.message);
|
|
81
86
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/security/api/mfa/_shared.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport '../../lib/mfaCompletionRoutes'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { MfaService, MfaServiceError } from '../../services/MfaService'\nimport type { MfaVerificationService, MfaVerificationServiceError } from '../../services/MfaVerificationService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'mfa' })\nconst MFA_MANAGE_FEATURE = 'security.mfa.manage'\n\nconst jsonRecordSchema = z.record(z.string(), z.unknown())\n\nexport type MfaRequestContext = {\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n container: Awaited<ReturnType<typeof createRequestContainer>>\n commandContext: CommandRuntimeContext\n mfaService: MfaService\n mfaVerificationService: MfaVerificationService\n}\n\ntype MfaEnrollmentRbacService = {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n}\n\ntype MfaEnrollmentEnforcementService = {\n checkUserCompliance: (userId: string) => Promise<{\n compliant: boolean\n enforced: boolean\n }>\n}\n\nexport async function resolveMfaRequestContext(req: Request): Promise<MfaRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n mfaService: container.resolve<MfaService>('mfaService'),\n mfaVerificationService: container.resolve<MfaVerificationService>('mfaVerificationService'),\n }\n}\n\nexport async function authorizeMfaEnrollmentMutation(\n context: Pick<MfaRequestContext, 'auth' | 'container'>,\n): Promise<NextResponse | null> {\n const tenantId = typeof context.auth.tenantId === 'string' && context.auth.tenantId.trim().length > 0\n ? context.auth.tenantId.trim()\n : null\n const organizationId = typeof context.auth.orgId === 'string' && context.auth.orgId.trim().length > 0\n ? context.auth.orgId.trim()\n : null\n const rbacService = context.container.resolve<MfaEnrollmentRbacService>('rbacService')\n const canManageMfa = await rbacService.userHasAllFeatures(\n context.auth.sub,\n [MFA_MANAGE_FEATURE],\n { tenantId, organizationId },\n )\n if (canManageMfa) return null\n if (!tenantId) return securityApiError(403, 'Forbidden')\n\n try {\n const enforcementService = context.container.resolve<MfaEnrollmentEnforcementService>('mfaEnforcementService')\n const compliance = await enforcementService.checkUserCompliance(context.auth.sub)\n if (compliance.enforced && !compliance.compliant) return null\n } catch (error) {\n logger.error('Unable to verify compelled MFA enrollment authorization; allowing enrollment recovery path', {\n err: error,\n })\n return null\n }\n\n return securityApiError(403, 'Forbidden')\n}\n\nexport async function readJsonRecord(req: Request): Promise<Record<string, unknown>> {\n try {\n const parsed = jsonRecordSchema.safeParse(await req.json())\n return parsed.success ? parsed.data : {}\n } catch {\n return {}\n }\n}\n\nexport function readUuidParam(value: string | string[] | undefined): string | null {\n if (typeof value !== 'string') return null\n const parsed = z.string().uuid().safeParse(value)\n return parsed.success ? parsed.data : null\n}\n\nexport function readString(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport async function mapMfaError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isMfaServiceError(error) || isMfaVerificationServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n logger.error('MFA route failure', { err: error })\n return securityApiError(500, 'Failed to process MFA request.')\n}\n\nfunction isMfaServiceError(error: unknown): error is MfaServiceError {\n return error instanceof Error\n && error.name === 'MfaServiceError'\n && typeof (error as Partial<MfaServiceError>).statusCode === 'number'\n}\n\nfunction isMfaVerificationServiceError(error: unknown): error is MfaVerificationServiceError {\n return error instanceof Error\n && error.name === 'MfaVerificationServiceError'\n && typeof (error as Partial<MfaVerificationServiceError>).statusCode === 'number'\n}\n\nexport function issueVerifiedMfaToken(auth: MfaRequestContext['auth'], methods: string[]): string {\n const nextPayload: Record<string, unknown> = {\n sub: auth.sub,\n sid: typeof auth.sid === 'string' ? auth.sid : undefined,\n tenantId: auth.tenantId ?? null,\n orgId: auth.orgId ?? null,\n email: typeof auth.email === 'string' ? auth.email : null,\n roles: Array.isArray(auth.roles) ? auth.roles : [],\n mfa_pending: false,\n mfa_verified: true,\n mfa_methods: methods,\n }\n if (typeof auth.actorTenantId === 'string') {\n nextPayload.actorTenantId = auth.actorTenantId\n }\n if (typeof auth.actorOrgId === 'string') {\n nextPayload.actorOrgId = auth.actorOrgId\n }\n if (auth.isSuperAdmin === true) {\n nextPayload.isSuperAdmin = true\n }\n return signJwt({\n ...nextPayload,\n })\n}\n\nexport function setAuthCookie(response: NextResponse, token: string): void {\n response.cookies.set('auth_token', token, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AACvC,OAAO;AACP,SAAS,0BAA0B;AACnC,SAAS,eAAe;AAGxB,SAAS,yBAAyB,wBAAwB;AAC1D,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,MAAM,CAAC;AAClE,MAAM,qBAAqB;AAE3B,MAAM,mBAAmB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAyBzD,eAAsB,yBAAyB,KAAyD;AACtG,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,iBAAiB,KAAK,cAAc;AAAA,EAC7C;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,MACd;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB,wBAAwB,KAAK,SAAS;AAAA,MACtC,iBAAiB,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,MAC7C,SAAS;AAAA,IACX;AAAA,IACA,YAAY,UAAU,QAAoB,YAAY;AAAA,IACtD,wBAAwB,UAAU,QAAgC,wBAAwB;AAAA,EAC5F;AACF;AAEA,eAAsB,+BACpB,SAC8B;AAC9B,QAAM,WAAW,OAAO,QAAQ,KAAK,aAAa,YAAY,QAAQ,KAAK,SAAS,KAAK,EAAE,SAAS,IAChG,QAAQ,KAAK,SAAS,KAAK,IAC3B;AACJ,QAAM,iBAAiB,OAAO,QAAQ,KAAK,UAAU,YAAY,QAAQ,KAAK,MAAM,KAAK,EAAE,SAAS,IAChG,QAAQ,KAAK,MAAM,KAAK,IACxB;AACJ,QAAM,cAAc,QAAQ,UAAU,QAAkC,aAAa;AACrF,QAAM,eAAe,MAAM,YAAY;AAAA,IACrC,QAAQ,KAAK;AAAA,IACb,CAAC,kBAAkB;AAAA,IACnB,EAAE,UAAU,eAAe;AAAA,EAC7B;AACA,MAAI,aAAc,QAAO;AACzB,MAAI,CAAC,SAAU,QAAO,iBAAiB,KAAK,WAAW;AAEvD,MAAI;AACF,UAAM,qBAAqB,QAAQ,UAAU,QAAyC,uBAAuB;AAC7G,UAAM,aAAa,MAAM,mBAAmB,oBAAoB,QAAQ,KAAK,GAAG;AAChF,QAAI,WAAW,YAAY,CAAC,WAAW,UAAW,QAAO;AAAA,EAC3D,SAAS,OAAO;AACd,WAAO,MAAM,8FAA8F;AAAA,MACzG,KAAK;AAAA,IACP,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO,iBAAiB,KAAK,WAAW;AAC1C;AAEA,eAAsB,eAAe,KAAgD;AACnF,MAAI;AACF,UAAM,SAAS,iBAAiB,UAAU,MAAM,IAAI,KAAK,CAAC;AAC1D,WAAO,OAAO,UAAU,OAAO,OAAO,CAAC;AAAA,EACzC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,cAAc,OAAqD;AACjF,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,KAAK;AAChD,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;AAEO,SAAS,WAAW,OAA+B;AACxD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAEA,eAAsB,YAAY,OAAuC;AACvE,MAAI,iBAAiB,eAAe;AAClC,WAAO,aAAa,KAAK,MAAM,wBAAwB,MAAM,IAAI,GAAG,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,EAC9F;AACA,MAAI,kBAAkB,KAAK,KAAK,8BAA8B,KAAK,GAAG;AACpE,WAAO,iBAAiB,MAAM,YAAY,MAAM,OAAO;AAAA,EACzD;AACA,SAAO,MAAM,qBAAqB,EAAE,KAAK,MAAM,CAAC;AAChD,SAAO,iBAAiB,KAAK,gCAAgC;AAC/D;AAEA,SAAS,kBAAkB,OAA0C;AACnE,SAAO,iBAAiB,SACnB,MAAM,SAAS,qBACf,OAAQ,MAAmC,eAAe;AACjE;AAEA,SAAS,8BAA8B,OAAsD;AAC3F,SAAO,iBAAiB,SACnB,MAAM,SAAS,iCACf,OAAQ,MAA+C,eAAe;AAC7E;AAEO,SAAS,sBAAsB,MAAiC,SAA2B;AAChG,QAAM,cAAuC;AAAA,IAC3C,KAAK,KAAK;AAAA,IACV,KAAK,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;AAAA,IAC/C,UAAU,KAAK,YAAY;AAAA,IAC3B,OAAO,KAAK,SAAS;AAAA,IACrB,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,IACrD,OAAO,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AAAA,IACjD,aAAa;AAAA,IACb,cAAc;AAAA,IACd,aAAa;AAAA,EACf;AACA,MAAI,OAAO,KAAK,kBAAkB,UAAU;AAC1C,gBAAY,gBAAgB,KAAK;AAAA,EACnC;AACA,MAAI,OAAO,KAAK,eAAe,UAAU;AACvC,gBAAY,aAAa,KAAK;AAAA,EAChC;AACA,MAAI,KAAK,iBAAiB,MAAM;AAC9B,gBAAY,eAAe;AAAA,EAC7B;AACA,SAAO,QAAQ;AAAA,IACb,GAAG;AAAA,EACL,CAAC;AACH;AAEO,SAAS,cAAc,UAAwB,OAAqB;AACzE,WAAS,QAAQ,IAAI,cAAc,OAAO;AAAA,IACxC,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,QAAQ,KAAK,KAAK;AAAA,EACpB,CAAC;AACH;",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport '../../lib/mfaCompletionRoutes'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { MfaService, MfaServiceError } from '../../services/MfaService'\nimport type { MfaVerificationService, MfaVerificationServiceError } from '../../services/MfaVerificationService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'mfa' })\nconst MFA_MANAGE_FEATURE = 'security.mfa.manage'\n\nconst jsonRecordSchema = z.record(z.string(), z.unknown())\n\nexport type MfaRequestContext = {\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n container: Awaited<ReturnType<typeof createRequestContainer>>\n commandContext: CommandRuntimeContext\n mfaService: MfaService\n mfaVerificationService: MfaVerificationService\n}\n\ntype MfaEnrollmentRbacService = {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n}\n\ntype MfaEnrollmentEnforcementService = {\n checkUserCompliance: (userId: string) => Promise<{\n compliant: boolean\n enforced: boolean\n }>\n}\n\nexport async function resolveMfaRequestContext(req: Request): Promise<MfaRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n mfaService: container.resolve<MfaService>('mfaService'),\n mfaVerificationService: container.resolve<MfaVerificationService>('mfaVerificationService'),\n }\n}\n\nexport async function authorizeMfaEnrollmentMutation(\n context: Pick<MfaRequestContext, 'auth' | 'container'>,\n): Promise<NextResponse | null> {\n const tenantId = typeof context.auth.tenantId === 'string' && context.auth.tenantId.trim().length > 0\n ? context.auth.tenantId.trim()\n : null\n const organizationId = typeof context.auth.orgId === 'string' && context.auth.orgId.trim().length > 0\n ? context.auth.orgId.trim()\n : null\n const rbacService = context.container.resolve<MfaEnrollmentRbacService>('rbacService')\n const canManageMfa = await rbacService.userHasAllFeatures(\n context.auth.sub,\n [MFA_MANAGE_FEATURE],\n { tenantId, organizationId },\n )\n if (canManageMfa) return null\n if (!tenantId) return securityApiError(403, 'Forbidden')\n\n try {\n const enforcementService = context.container.resolve<MfaEnrollmentEnforcementService>('mfaEnforcementService')\n const compliance = await enforcementService.checkUserCompliance(context.auth.sub)\n if (compliance.enforced && !compliance.compliant) return null\n } catch (error) {\n logger.error('Unable to verify compelled MFA enrollment authorization; allowing enrollment recovery path', {\n err: error,\n })\n return null\n }\n\n return securityApiError(403, 'Forbidden')\n}\n\nexport async function readJsonRecord(req: Request): Promise<Record<string, unknown>> {\n try {\n const parsed = jsonRecordSchema.safeParse(await req.json())\n return parsed.success ? parsed.data : {}\n } catch {\n return {}\n }\n}\n\nexport function readUuidParam(value: string | string[] | undefined): string | null {\n if (typeof value !== 'string') return null\n const parsed = z.string().uuid().safeParse(value)\n return parsed.success ? parsed.data : null\n}\n\nexport function readString(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport async function mapMfaError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (isMfaServiceError(error) || isMfaVerificationServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n logger.error('MFA route failure', { err: error })\n return securityApiError(500, 'Failed to process MFA request.')\n}\n\nfunction isMfaServiceError(error: unknown): error is MfaServiceError {\n return error instanceof Error\n && error.name === 'MfaServiceError'\n && typeof (error as Partial<MfaServiceError>).statusCode === 'number'\n}\n\nfunction isMfaVerificationServiceError(error: unknown): error is MfaVerificationServiceError {\n return error instanceof Error\n && error.name === 'MfaVerificationServiceError'\n && typeof (error as Partial<MfaVerificationServiceError>).statusCode === 'number'\n}\n\nexport function issueVerifiedMfaToken(auth: MfaRequestContext['auth'], methods: string[]): string {\n const nextPayload: Record<string, unknown> = {\n sub: auth.sub,\n sid: typeof auth.sid === 'string' ? auth.sid : undefined,\n tenantId: auth.tenantId ?? null,\n orgId: auth.orgId ?? null,\n email: typeof auth.email === 'string' ? auth.email : null,\n roles: Array.isArray(auth.roles) ? auth.roles : [],\n mfa_pending: false,\n mfa_verified: true,\n mfa_methods: methods,\n }\n if (typeof auth.actorTenantId === 'string') {\n nextPayload.actorTenantId = auth.actorTenantId\n }\n if (typeof auth.actorOrgId === 'string') {\n nextPayload.actorOrgId = auth.actorOrgId\n }\n if (auth.isSuperAdmin === true) {\n nextPayload.isSuperAdmin = true\n }\n return signJwt({\n ...nextPayload,\n })\n}\n\nexport function setAuthCookie(response: NextResponse, token: string): void {\n response.cookies.set('auth_token', token, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,7 @@ import { changePasswordSchema } from "../../../data/validators.js";
|
|
|
8
8
|
import { buildSecurityOpenApi, securityErrorSchema } from "../../openapi.js";
|
|
9
9
|
import { localizeSecurityApiBody } from "../../i18n.js";
|
|
10
10
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
11
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
11
12
|
const logger = createLogger("security").child({ component: "profile-password" });
|
|
12
13
|
const changePasswordResponseSchema = z.object({
|
|
13
14
|
ok: z.literal(true)
|
|
@@ -59,6 +60,10 @@ async function PUT(req) {
|
|
|
59
60
|
if (error instanceof CrudHttpError) {
|
|
60
61
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status });
|
|
61
62
|
}
|
|
63
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error);
|
|
64
|
+
if (interceptorRejection) {
|
|
65
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
66
|
+
}
|
|
62
67
|
logger.error("Profile password update failed", { err: error });
|
|
63
68
|
return NextResponse.json(
|
|
64
69
|
{ error: translate("security.profile.password.form.errors.save", "Failed to update password.") },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/security/api/profile/password/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { changePasswordSchema } from '../../../data/validators'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { localizeSecurityApiBody } from '../../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'profile-password' })\n\nconst changePasswordResponseSchema = z.object({\n ok: z.literal(true),\n})\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nfunction buildCommandContext(container: RequestContainer, auth: Auth, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport const metadata = {\n PUT: { requireAuth: true, requireFeatures: ['security.profile.password'] },\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json(\n { error: translate('api.errors.unauthorized', 'Unauthorized') },\n { status: 401 },\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n body = {}\n }\n\n const parsed = changePasswordSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: translate('api.errors.invalidPayload', 'Invalid payload.'), issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n try {\n const container = await createRequestContainer()\n const commandBus = container.resolve<CommandBus>('commandBus')\n await commandBus.execute('security.password.change', {\n input: parsed.data,\n ctx: buildCommandContext(container, auth, req),\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n logger.error('Profile password update failed', { err: error })\n return NextResponse.json(\n { error: translate('security.profile.password.form.errors.save', 'Failed to update password.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Security password routes',\n methods: {\n PUT: {\n summary: 'Change current user password',\n description: 'Changes password for the authenticated user and requires the current password.',\n requestBody: {\n contentType: 'application/json',\n schema: changePasswordSchema,\n },\n responses: [\n { status: 200, description: 'Password updated', schema: changePasswordResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or password validation error', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAC9B,SAAS,4BAA4B;AACrC,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { changePasswordSchema } from '../../../data/validators'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { localizeSecurityApiBody } from '../../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('security').child({ component: 'profile-password' })\n\nconst changePasswordResponseSchema = z.object({\n ok: z.literal(true),\n})\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nfunction buildCommandContext(container: RequestContainer, auth: Auth, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport const metadata = {\n PUT: { requireAuth: true, requireFeatures: ['security.profile.password'] },\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json(\n { error: translate('api.errors.unauthorized', 'Unauthorized') },\n { status: 401 },\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n body = {}\n }\n\n const parsed = changePasswordSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: translate('api.errors.invalidPayload', 'Invalid payload.'), issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n try {\n const container = await createRequestContainer()\n const commandBus = container.resolve<CommandBus>('commandBus')\n await commandBus.execute('security.password.change', {\n input: parsed.data,\n ctx: buildCommandContext(container, auth, req),\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Profile password update failed', { err: error })\n return NextResponse.json(\n { error: translate('security.profile.password.form.errors.save', 'Failed to update password.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Security password routes',\n methods: {\n PUT: {\n summary: 'Change current user password',\n description: 'Changes password for the authenticated user and requires the current password.',\n requestBody: {\n contentType: 'application/json',\n schema: changePasswordSchema,\n },\n responses: [\n { status: 200, description: 'Password updated', schema: changePasswordResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or password validation error', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
3
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
3
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
5
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
6
|
import { Organization, Tenant } from "@open-mercato/core/modules/directory/data/entities";
|
|
@@ -95,6 +96,10 @@ async function mapSudoError(error) {
|
|
|
95
96
|
if (error instanceof CrudHttpError) {
|
|
96
97
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status });
|
|
97
98
|
}
|
|
99
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error);
|
|
100
|
+
if (interceptorRejection) {
|
|
101
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
102
|
+
}
|
|
98
103
|
if (isSudoRequiredError(error)) {
|
|
99
104
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode });
|
|
100
105
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/security/api/sudo/_shared.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\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 { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport type { SudoChallengeConfig } from '../../data/entities'\nimport type {\n SudoAuthScope,\n SudoChallengeService,\n SudoChallengeServiceError,\n} from '../../services/SudoChallengeService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'sudo' })\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SudoRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n sudoChallengeService: SudoChallengeService\n scope: SudoAuthScope\n}\n\nexport function buildSudoAuthScope(auth: Auth): SudoAuthScope {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: auth.isSuperAdmin === true,\n }\n}\n\nexport function toSudoConfigResponse(config: SudoChallengeConfig) {\n return {\n id: config.id,\n tenantId: config.tenantId ?? null,\n tenantName: null as string | null,\n organizationId: config.organizationId ?? null,\n organizationName: null as string | null,\n label: config.label ?? null,\n targetIdentifier: config.targetIdentifier,\n isEnabled: config.isEnabled,\n isDeveloperDefault: config.isDeveloperDefault,\n ttlSeconds: config.ttlSeconds,\n challengeMethod: config.challengeMethod,\n configuredBy: config.configuredBy ?? null,\n createdAt: config.createdAt.toISOString(),\n updatedAt: config.updatedAt.toISOString(),\n }\n}\n\nexport async function attachSudoConfigScopeNames(\n container: RequestContainer,\n configs: SudoChallengeConfig[],\n): Promise<Array<ReturnType<typeof toSudoConfigResponse>>> {\n if (configs.length === 0) return []\n\n const em = container.resolve<EntityManager>('em')\n const tenantIds = Array.from(\n new Set(\n configs\n .map((c) => c.tenantId ?? null)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n const organizationIds = Array.from(\n new Set(\n configs\n .map((c) => c.organizationId ?? null)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n\n const [tenants, organizations] = await Promise.all([\n tenantIds.length ? em.find(Tenant, { id: { $in: tenantIds }, deletedAt: null }) : Promise.resolve([]),\n organizationIds.length ? em.find(Organization, { id: { $in: organizationIds }, deletedAt: null }) : Promise.resolve([]),\n ])\n\n const tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const id = tenant?.id ? String(tenant.id) : null\n if (!id) return acc\n acc[id] = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : id\n return acc\n }, {})\n const organizationMap = organizations.reduce<Record<string, string>>((acc, org) => {\n const id = org?.id ? String(org.id) : null\n if (!id) return acc\n acc[id] = typeof org.name === 'string' && org.name.length > 0 ? org.name : id\n return acc\n }, {})\n\n return configs.map((config) => {\n const response = toSudoConfigResponse(config)\n return {\n ...response,\n tenantName: response.tenantId ? tenantMap[response.tenantId] ?? response.tenantId : null,\n organizationName: response.organizationId ? organizationMap[response.organizationId] ?? response.organizationId : null,\n }\n })\n}\n\nexport async function resolveSudoContext(req: Request): Promise<SudoRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n sudoChallengeService: container.resolve<SudoChallengeService>('sudoChallengeService'),\n scope: buildSudoAuthScope(auth),\n }\n}\n\nexport async function mapSudoError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isSudoChallengeServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n logger.error('Sudo route failure', { err: error })\n return securityApiError(500, 'Failed to process sudo request.')\n}\n\nfunction isSudoChallengeServiceError(error: unknown): error is SudoChallengeServiceError {\n return error instanceof Error\n && error.name === 'SudoChallengeServiceError'\n && typeof (error as Partial<SudoChallengeServiceError>).statusCode === 'number'\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport type { SudoChallengeConfig } from '../../data/entities'\nimport type {\n SudoAuthScope,\n SudoChallengeService,\n SudoChallengeServiceError,\n} from '../../services/SudoChallengeService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'sudo' })\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SudoRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n sudoChallengeService: SudoChallengeService\n scope: SudoAuthScope\n}\n\nexport function buildSudoAuthScope(auth: Auth): SudoAuthScope {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: auth.isSuperAdmin === true,\n }\n}\n\nexport function toSudoConfigResponse(config: SudoChallengeConfig) {\n return {\n id: config.id,\n tenantId: config.tenantId ?? null,\n tenantName: null as string | null,\n organizationId: config.organizationId ?? null,\n organizationName: null as string | null,\n label: config.label ?? null,\n targetIdentifier: config.targetIdentifier,\n isEnabled: config.isEnabled,\n isDeveloperDefault: config.isDeveloperDefault,\n ttlSeconds: config.ttlSeconds,\n challengeMethod: config.challengeMethod,\n configuredBy: config.configuredBy ?? null,\n createdAt: config.createdAt.toISOString(),\n updatedAt: config.updatedAt.toISOString(),\n }\n}\n\nexport async function attachSudoConfigScopeNames(\n container: RequestContainer,\n configs: SudoChallengeConfig[],\n): Promise<Array<ReturnType<typeof toSudoConfigResponse>>> {\n if (configs.length === 0) return []\n\n const em = container.resolve<EntityManager>('em')\n const tenantIds = Array.from(\n new Set(\n configs\n .map((c) => c.tenantId ?? null)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n const organizationIds = Array.from(\n new Set(\n configs\n .map((c) => c.organizationId ?? null)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n\n const [tenants, organizations] = await Promise.all([\n tenantIds.length ? em.find(Tenant, { id: { $in: tenantIds }, deletedAt: null }) : Promise.resolve([]),\n organizationIds.length ? em.find(Organization, { id: { $in: organizationIds }, deletedAt: null }) : Promise.resolve([]),\n ])\n\n const tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const id = tenant?.id ? String(tenant.id) : null\n if (!id) return acc\n acc[id] = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : id\n return acc\n }, {})\n const organizationMap = organizations.reduce<Record<string, string>>((acc, org) => {\n const id = org?.id ? String(org.id) : null\n if (!id) return acc\n acc[id] = typeof org.name === 'string' && org.name.length > 0 ? org.name : id\n return acc\n }, {})\n\n return configs.map((config) => {\n const response = toSudoConfigResponse(config)\n return {\n ...response,\n tenantName: response.tenantId ? tenantMap[response.tenantId] ?? response.tenantId : null,\n organizationName: response.organizationId ? organizationMap[response.organizationId] ?? response.organizationId : null,\n }\n })\n}\n\nexport async function resolveSudoContext(req: Request): Promise<SudoRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n sudoChallengeService: container.resolve<SudoChallengeService>('sudoChallengeService'),\n scope: buildSudoAuthScope(auth),\n }\n}\n\nexport async function mapSudoError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isSudoChallengeServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n logger.error('Sudo route failure', { err: error })\n return securityApiError(500, 'Failed to process sudo request.')\n}\n\nfunction isSudoChallengeServiceError(error: unknown): error is SudoChallengeServiceError {\n return error instanceof Error\n && error.name === 'SudoChallengeServiceError'\n && typeof (error as Partial<SudoChallengeServiceError>).statusCode === 'number'\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { CrudHttpError, forbidden } from "@open-mercato/shared/lib/crud/errors";
|
|
3
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
3
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
5
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
6
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
@@ -83,6 +84,10 @@ async function mapSecurityUsersError(error) {
|
|
|
83
84
|
if (error instanceof CrudHttpError) {
|
|
84
85
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status });
|
|
85
86
|
}
|
|
87
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error);
|
|
88
|
+
if (interceptorRejection) {
|
|
89
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
90
|
+
}
|
|
86
91
|
if (isSudoRequiredError(error)) {
|
|
87
92
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode });
|
|
88
93
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/security/api/users/_shared.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport type { MfaAdminService, MfaAdminServiceError } from '../../services/MfaAdminService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'users' })\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SecurityUsersRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n mfaAdminService: MfaAdminService\n}\n\nexport async function resolveSecurityUsersContext(\n req: Request,\n): Promise<SecurityUsersRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n mfaAdminService: container.resolve<MfaAdminService>('mfaAdminService'),\n }\n}\n\nfunction normalizeNullableString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction normalizeOrganizationList(values: unknown): string[] | null {\n if (values === null || values === undefined) return null\n if (!Array.isArray(values)) return null\n const result: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (trimmed) result.push(trimmed)\n }\n return result\n}\n\nexport async function assertActorCanAccessSecurityUserTarget(\n ctx: SecurityUsersRequestContext,\n targetUserId: string,\n): Promise<void> {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (isSuperAdmin) return\n\n const em = ctx.container.resolve<EntityManager>('em')\n const target = await findOneWithDecryption(\n em,\n User,\n { id: targetUserId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n if (!target) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const actorTenantId = normalizeNullableString(ctx.auth.tenantId)\n const targetTenantId = normalizeNullableString((target as { tenantId?: string | null }).tenantId)\n if (!targetTenantId || targetTenantId !== actorTenantId) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const rbacService = ctx.container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(ctx.auth.sub, {\n tenantId: actorTenantId,\n organizationId: normalizeNullableString(ctx.auth.orgId),\n })\n const organizations = normalizeOrganizationList(acl?.organizations)\n if (organizations !== null && !organizations.includes('__all__')) {\n const targetOrganizationId = normalizeNullableString((target as { organizationId?: string | null }).organizationId)\n if (!targetOrganizationId || !organizations.includes(targetOrganizationId)) {\n throw forbidden('Not authorized to access this user.')\n }\n }\n}\n\nexport async function assertActorOwnsTenantScope(\n ctx: SecurityUsersRequestContext,\n requestedTenantId: string | null | undefined,\n): Promise<string | null> {\n const resolved = await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, requestedTenantId)\n return resolved ?? ctx.auth.tenantId ?? null\n}\n\nexport async function mapSecurityUsersError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isMfaAdminServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n\n logger.error('Users route failure', { err: error })\n return securityApiError(500, 'Failed to process user security request.')\n}\n\nfunction isMfaAdminServiceError(error: unknown): error is MfaAdminServiceError {\n return error instanceof Error\n && error.name === 'MfaAdminServiceError'\n && typeof (error as Partial<MfaAdminServiceError>).statusCode === 'number'\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,eAAe,iBAAiB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport type { MfaAdminService, MfaAdminServiceError } from '../../services/MfaAdminService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'users' })\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SecurityUsersRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n mfaAdminService: MfaAdminService\n}\n\nexport async function resolveSecurityUsersContext(\n req: Request,\n): Promise<SecurityUsersRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n mfaAdminService: container.resolve<MfaAdminService>('mfaAdminService'),\n }\n}\n\nfunction normalizeNullableString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction normalizeOrganizationList(values: unknown): string[] | null {\n if (values === null || values === undefined) return null\n if (!Array.isArray(values)) return null\n const result: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (trimmed) result.push(trimmed)\n }\n return result\n}\n\nexport async function assertActorCanAccessSecurityUserTarget(\n ctx: SecurityUsersRequestContext,\n targetUserId: string,\n): Promise<void> {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (isSuperAdmin) return\n\n const em = ctx.container.resolve<EntityManager>('em')\n const target = await findOneWithDecryption(\n em,\n User,\n { id: targetUserId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n if (!target) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const actorTenantId = normalizeNullableString(ctx.auth.tenantId)\n const targetTenantId = normalizeNullableString((target as { tenantId?: string | null }).tenantId)\n if (!targetTenantId || targetTenantId !== actorTenantId) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const rbacService = ctx.container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(ctx.auth.sub, {\n tenantId: actorTenantId,\n organizationId: normalizeNullableString(ctx.auth.orgId),\n })\n const organizations = normalizeOrganizationList(acl?.organizations)\n if (organizations !== null && !organizations.includes('__all__')) {\n const targetOrganizationId = normalizeNullableString((target as { organizationId?: string | null }).organizationId)\n if (!targetOrganizationId || !organizations.includes(targetOrganizationId)) {\n throw forbidden('Not authorized to access this user.')\n }\n }\n}\n\nexport async function assertActorOwnsTenantScope(\n ctx: SecurityUsersRequestContext,\n requestedTenantId: string | null | undefined,\n): Promise<string | null> {\n const resolved = await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, requestedTenantId)\n return resolved ?? ctx.auth.tenantId ?? null\n}\n\nexport async function mapSecurityUsersError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isMfaAdminServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n\n logger.error('Users route failure', { err: error })\n return securityApiError(500, 'Failed to process user security request.')\n}\n\nfunction isMfaAdminServiceError(error: unknown): error is MfaAdminServiceError {\n return error instanceof Error\n && error.name === 'MfaAdminServiceError'\n && typeof (error as Partial<MfaAdminServiceError>).statusCode === 'number'\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/jest.config.cjs
CHANGED
|
@@ -9,6 +9,8 @@ module.exports = {
|
|
|
9
9
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
10
10
|
moduleNameMapper: {
|
|
11
11
|
'^@open-mercato/enterprise/(.*)$': '<rootDir>/src/$1',
|
|
12
|
+
'^@open-mercato/cache$': '<rootDir>/../cache/src/index.ts',
|
|
13
|
+
'^@open-mercato/cache/(.*)$': '<rootDir>/../cache/src/$1',
|
|
12
14
|
'^@open-mercato/core/(.*)$': '<rootDir>/../core/src/$1',
|
|
13
15
|
'^@open-mercato/shared/(.*)$': '<rootDir>/../shared/src/$1',
|
|
14
16
|
'^@open-mercato/ui/(.*)$': '<rootDir>/../ui/src/$1',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7131.1.20abf9e885",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@open-mercato/core": "0.7.1-develop.
|
|
69
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
68
|
+
"@open-mercato/core": "0.7.1-develop.7131.1.20abf9e885",
|
|
69
|
+
"@open-mercato/ui": "0.7.1-develop.7131.1.20abf9e885",
|
|
70
70
|
"@simplewebauthn/browser": "^13.3.0",
|
|
71
71
|
"@simplewebauthn/server": "^13.3.2",
|
|
72
72
|
"@simplewebauthn/types": "^12.0.0",
|
|
@@ -76,12 +76,12 @@
|
|
|
76
76
|
"qrcode": "^1.5.4"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
79
|
+
"@open-mercato/shared": "0.7.1-develop.7131.1.20abf9e885",
|
|
80
80
|
"react": "^19.0.0",
|
|
81
81
|
"react-dom": "^19.0.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
84
|
+
"@open-mercato/shared": "0.7.1-develop.7131.1.20abf9e885",
|
|
85
85
|
"@types/jest": "^30.0.0",
|
|
86
86
|
"@types/react": "^19.2.17",
|
|
87
87
|
"@types/react-dom": "^19.2.3",
|
|
@@ -3,6 +3,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
3
3
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
4
4
|
import { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
5
5
|
import { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'
|
|
6
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
6
7
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
7
8
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
8
9
|
import { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
@@ -128,6 +129,10 @@ export async function mapEnforcementError(error: unknown): Promise<NextResponse>
|
|
|
128
129
|
if (error instanceof CrudHttpError) {
|
|
129
130
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })
|
|
130
131
|
}
|
|
132
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error)
|
|
133
|
+
if (interceptorRejection) {
|
|
134
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
135
|
+
}
|
|
131
136
|
if (isMfaEnforcementServiceError(error)) {
|
|
132
137
|
return securityApiError(error.statusCode, error.message)
|
|
133
138
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
4
4
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
5
6
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
7
|
import '../../lib/mfaCompletionRoutes'
|
|
7
8
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
@@ -119,6 +120,10 @@ export async function mapMfaError(error: unknown): Promise<NextResponse> {
|
|
|
119
120
|
if (error instanceof CrudHttpError) {
|
|
120
121
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })
|
|
121
122
|
}
|
|
123
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error)
|
|
124
|
+
if (interceptorRejection) {
|
|
125
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
126
|
+
}
|
|
122
127
|
if (isMfaServiceError(error) || isMfaVerificationServiceError(error)) {
|
|
123
128
|
return securityApiError(error.statusCode, error.message)
|
|
124
129
|
}
|
|
@@ -9,6 +9,7 @@ import { changePasswordSchema } from '../../../data/validators'
|
|
|
9
9
|
import { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'
|
|
10
10
|
import { localizeSecurityApiBody } from '../../i18n'
|
|
11
11
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
12
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
12
13
|
|
|
13
14
|
const logger = createLogger('security').child({ component: 'profile-password' })
|
|
14
15
|
|
|
@@ -71,6 +72,10 @@ export async function PUT(req: Request) {
|
|
|
71
72
|
if (error instanceof CrudHttpError) {
|
|
72
73
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })
|
|
73
74
|
}
|
|
75
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error)
|
|
76
|
+
if (interceptorRejection) {
|
|
77
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
78
|
+
}
|
|
74
79
|
logger.error('Profile password update failed', { err: error })
|
|
75
80
|
return NextResponse.json(
|
|
76
81
|
{ error: translate('security.profile.password.form.errors.save', 'Failed to update password.') },
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
3
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
4
4
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
5
6
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
7
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
8
|
import { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
@@ -132,6 +133,10 @@ export async function mapSudoError(error: unknown): Promise<NextResponse> {
|
|
|
132
133
|
if (error instanceof CrudHttpError) {
|
|
133
134
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })
|
|
134
135
|
}
|
|
136
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error)
|
|
137
|
+
if (interceptorRejection) {
|
|
138
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
139
|
+
}
|
|
135
140
|
if (isSudoRequiredError(error)) {
|
|
136
141
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })
|
|
137
142
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
3
3
|
import { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'
|
|
4
|
+
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
4
5
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
5
6
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
7
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
@@ -116,6 +117,10 @@ export async function mapSecurityUsersError(error: unknown): Promise<NextRespons
|
|
|
116
117
|
if (error instanceof CrudHttpError) {
|
|
117
118
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })
|
|
118
119
|
}
|
|
120
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(error)
|
|
121
|
+
if (interceptorRejection) {
|
|
122
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
123
|
+
}
|
|
119
124
|
if (isSudoRequiredError(error)) {
|
|
120
125
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })
|
|
121
126
|
}
|