@open-mercato/core 0.10.1-develop.7691.1.d8af406541 → 0.10.1-develop.7701.1.87b1ad4224
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/attachments/api/file/[id]/route.js +1 -1
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +1 -1
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/auth/api/autologin.js +3 -1
- package/dist/modules/auth/api/autologin.js.map +2 -2
- package/dist/modules/auth/api/login.js +4 -1
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/lib/sessionIntegrity.js +2 -0
- package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
- package/dist/modules/auth/lib/tenantEligibility.js +16 -0
- package/dist/modules/auth/lib/tenantEligibility.js.map +7 -0
- package/dist/modules/auth/services/authService.js +5 -0
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +5 -5
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +5 -5
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js +36 -14
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +2 -2
- package/dist/modules/customer_accounts/api/login.js +2 -1
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js +2 -0
- package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
- package/dist/modules/customer_accounts/lib/tenantEligibility.js +16 -0
- package/dist/modules/customer_accounts/lib/tenantEligibility.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +2 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerSessionService.js +2 -0
- package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerTokenService.js +2 -0
- package/dist/modules/customer_accounts/services/customerTokenService.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +5 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
- package/dist/modules/customers/components/linking/LinkEntityDialog.js +10 -2
- package/dist/modules/customers/components/linking/LinkEntityDialog.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +17 -3
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/api/file/[id]/route.ts +1 -1
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +1 -1
- package/src/modules/auth/api/autologin.ts +3 -1
- package/src/modules/auth/api/login.ts +4 -1
- package/src/modules/auth/lib/sessionIntegrity.ts +2 -0
- package/src/modules/auth/lib/tenantEligibility.ts +17 -0
- package/src/modules/auth/services/authService.ts +5 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +5 -5
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +5 -5
- package/src/modules/customer_accounts/api/admin/domain-mappings.ts +38 -16
- package/src/modules/customer_accounts/api/login.ts +3 -1
- package/src/modules/customer_accounts/lib/customerAuth.ts +2 -0
- package/src/modules/customer_accounts/lib/tenantEligibility.ts +14 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +2 -0
- package/src/modules/customer_accounts/services/customerSessionService.ts +2 -0
- package/src/modules/customer_accounts/services/customerTokenService.ts +2 -0
- package/src/modules/customer_accounts/services/domainMappingService.ts +3 -0
- package/src/modules/customers/components/linking/LinkEntityDialog.tsx +10 -2
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +15 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customer_accounts/api/admin/domain-mappings/%5Bid%5D/health-check.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nconst FEATURE = 'customer_accounts.domain.manage'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function POST(req: Request, ctx: { params: Promise<{ id: string }> }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAGP,MAAM,UAAU;AAET,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,OAAO,EAAE;AACxD;AAEA,eAAsB,KAAK,KAAc,KAA0C;AACjF,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,SAAU,QAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAE3G,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,UAAU,QAAQ,aAAa;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nconst FEATURE = 'customer_accounts.domain.manage'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function POST(req: Request, ctx: { params: Promise<{ id: string }> }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n\n const { id } = await ctx.params\n const service = container.resolve('domainMappingService') as DomainMappingService\n const record = await service.findById(id, { tenantId: auth.tenantId })\n if (!record) return NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 })\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: record.organizationId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: record.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let updated\n try {\n updated = await service.healthCheck(id)\n } catch (err) {\n return NextResponse.json(\n { ok: false, error: err instanceof Error ? err.message : 'Health check failed' },\n { status: 500 },\n )\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: record.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n ok: true,\n domainMapping: {\n id: updated.id,\n hostname: updated.hostname,\n status: updated.status,\n tlsRetryCount: updated.tlsRetryCount,\n tlsFailureReason: updated.tlsFailureReason ?? null,\n },\n })\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'TLS health check for a custom domain mapping',\n methods: {\n POST: {\n summary: 'Trigger TLS health check',\n description:\n 'Runs an HTTPS probe to verify Traefik has provisioned a certificate. On success: transitions the mapping to active.',\n responses: [\n {\n status: 200,\n description: 'OK',\n schema: z.object({\n ok: z.literal(true),\n domainMapping: z.object({\n id: z.string().uuid(),\n hostname: z.string(),\n status: z.string(),\n tlsRetryCount: z.number().int().nonnegative(),\n tlsFailureReason: z.string().nullable(),\n }),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 404, description: 'Not found', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -15,15 +15,15 @@ async function POST(req, ctx) {
|
|
|
15
15
|
if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
|
16
16
|
const container = await createRequestContainer();
|
|
17
17
|
const rbac = container.resolve("rbacService");
|
|
18
|
-
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
19
|
-
tenantId: auth.tenantId,
|
|
20
|
-
organizationId: auth.orgId
|
|
21
|
-
});
|
|
22
|
-
if (!allowed) return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
|
23
18
|
const { id } = await ctx.params;
|
|
24
19
|
const service = container.resolve("domainMappingService");
|
|
25
20
|
const record = await service.findById(id, { tenantId: auth.tenantId });
|
|
26
21
|
if (!record) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
|
22
|
+
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
23
|
+
tenantId: auth.tenantId,
|
|
24
|
+
organizationId: record.organizationId
|
|
25
|
+
});
|
|
26
|
+
if (!allowed) return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
|
27
27
|
const guardResult = await validateCrudMutationGuard(container, {
|
|
28
28
|
tenantId: auth.tenantId,
|
|
29
29
|
organizationId: record.organizationId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customer_accounts/api/admin/domain-mappings/%5Bid%5D/verify.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nconst FEATURE = 'customer_accounts.domain.manage'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function POST(req: Request, ctx: { params: Promise<{ id: string }> }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAGP,MAAM,UAAU;AAET,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,OAAO,EAAE;AACxD;AAEA,eAAsB,KAAK,KAAc,KAA0C;AACjF,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,SAAU,QAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAE3G,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,UAAU,QAAQ,aAAa;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nconst FEATURE = 'customer_accounts.domain.manage'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nexport async function POST(req: Request, ctx: { params: Promise<{ id: string }> }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n\n const { id } = await ctx.params\n const service = container.resolve('domainMappingService') as DomainMappingService\n const record = await service.findById(id, { tenantId: auth.tenantId })\n if (!record) return NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 })\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: record.organizationId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: record.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let result\n try {\n result = await service.verify(id)\n } catch (err) {\n return NextResponse.json(\n { ok: false, error: err instanceof Error ? err.message : 'DNS verification failed' },\n { status: 500 },\n )\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: record.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n ok: true,\n domainMapping: {\n id: result.domainMapping.id,\n hostname: result.domainMapping.hostname,\n status: result.domainMapping.status,\n verifiedAt: result.domainMapping.verifiedAt?.toISOString() ?? null,\n lastDnsCheckAt: result.domainMapping.lastDnsCheckAt?.toISOString() ?? null,\n dnsFailureReason: result.domainMapping.dnsFailureReason ?? null,\n },\n diagnostics: result.diagnostics ?? null,\n })\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'Verify a custom domain mapping (DNS check)',\n methods: {\n POST: {\n summary: 'Trigger DNS verification',\n description:\n 'Runs DNS verification (CNAME \u2192 A \u2192 reverse-resolve fallback) for the domain mapping. Returns diagnostics on failure.',\n responses: [\n {\n status: 200,\n description: 'OK \u2014 see status',\n schema: z.object({\n ok: z.literal(true),\n domainMapping: z.object({\n id: z.string().uuid(),\n hostname: z.string(),\n status: z.string(),\n verifiedAt: z.string().nullable(),\n lastDnsCheckAt: z.string().nullable(),\n dnsFailureReason: z.string().nullable(),\n }),\n diagnostics: z.unknown().nullable(),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 404, description: 'Not found', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAGP,MAAM,UAAU;AAET,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,OAAO,EAAE;AACxD;AAEA,eAAsB,KAAK,KAAc,KAA0C;AACjF,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,SAAU,QAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAE3G,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,UAAU,QAAQ,aAAa;AAE5C,QAAM,EAAE,GAAG,IAAI,MAAM,IAAI;AACzB,QAAM,UAAU,UAAU,QAAQ,sBAAsB;AACxD,QAAM,SAAS,MAAM,QAAQ,SAAS,IAAI,EAAE,UAAU,KAAK,SAAS,CAAC;AACrE,MAAI,CAAC,OAAQ,QAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AACxF,QAAM,UAAU,MAAM,KAAK,mBAAmB,KAAK,KAAK,CAAC,OAAO,GAAG;AAAA,IACjE,UAAU,KAAK;AAAA,IACf,gBAAgB,OAAO;AAAA,EACzB,CAAC;AACD,MAAI,CAAC,QAAS,QAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAEzF,QAAM,cAAc,MAAM,0BAA0B,WAAW;AAAA,IAC7D,UAAU,KAAK;AAAA,IACf,gBAAgB,OAAO;AAAA,IACvB,QAAQ,KAAK;AAAA,IACb,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,eAAe,IAAI;AAAA,IACnB,gBAAgB,IAAI;AAAA,EACtB,CAAC;AACD,MAAI,eAAe,CAAC,YAAY,IAAI;AAClC,WAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,EAC3E;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,QAAQ,OAAO,EAAE;AAAA,EAClC,SAAS,KAAK;AACZ,WAAO,aAAa;AAAA,MAClB,EAAE,IAAI,OAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,0BAA0B;AAAA,MACnF,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,MAAI,aAAa,MAAM,YAAY,uBAAuB;AACxD,UAAM,iCAAiC,WAAW;AAAA,MAChD,UAAU,KAAK;AAAA,MACf,gBAAgB,OAAO;AAAA,MACvB,QAAQ,KAAK;AAAA,MACb,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,UAAU,YAAY,YAAY;AAAA,IACpC,CAAC;AAAA,EACH;AAEA,SAAO,aAAa,KAAK;AAAA,IACvB,IAAI;AAAA,IACJ,eAAe;AAAA,MACb,IAAI,OAAO,cAAc;AAAA,MACzB,UAAU,OAAO,cAAc;AAAA,MAC/B,QAAQ,OAAO,cAAc;AAAA,MAC7B,YAAY,OAAO,cAAc,YAAY,YAAY,KAAK;AAAA,MAC9D,gBAAgB,OAAO,cAAc,gBAAgB,YAAY,KAAK;AAAA,MACtE,kBAAkB,OAAO,cAAc,oBAAoB;AAAA,IAC7D;AAAA,IACA,aAAa,OAAO,eAAe;AAAA,EACrC,CAAC;AACH;AAEA,MAAM,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;AAEjE,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO;AAAA,YACf,IAAI,EAAE,QAAQ,IAAI;AAAA,YAClB,eAAe,EAAE,OAAO;AAAA,cACtB,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,cACpB,UAAU,EAAE,OAAO;AAAA,cACnB,QAAQ,EAAE,OAAO;AAAA,cACjB,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,cAChC,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAAA,cACpC,kBAAkB,EAAE,OAAO,EAAE,SAAS;AAAA,YACxC,CAAC;AAAA,YACD,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,UACpC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,YAAY;AAAA,QAChE,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,YAAY;AAAA,QAC7D,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,YAAY;AAAA,MAC/D;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,7 @@ import { UniqueConstraintViolationException } from "@mikro-orm/core";
|
|
|
4
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
5
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
6
6
|
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
7
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
7
8
|
import {
|
|
8
9
|
validateCrudMutationGuard,
|
|
9
10
|
runCrudMutationGuardAfterSuccess
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
DomainMappingOrgScopeError
|
|
14
15
|
} from "@open-mercato/core/modules/customer_accounts/services/domainMappingService";
|
|
15
16
|
import { DomainMapping } from "@open-mercato/core/modules/customer_accounts/data/entities";
|
|
17
|
+
import { Organization } from "@open-mercato/core/modules/directory/data/entities";
|
|
16
18
|
const FEATURE = "customer_accounts.domain.manage";
|
|
17
19
|
const metadata = {
|
|
18
20
|
GET: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
@@ -49,7 +51,7 @@ function serializeRecord(record) {
|
|
|
49
51
|
}
|
|
50
52
|
async function GET(req) {
|
|
51
53
|
const auth = await getAuthFromRequest(req);
|
|
52
|
-
if (!auth) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
|
54
|
+
if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
|
53
55
|
const container = await createRequestContainer();
|
|
54
56
|
const rbac = container.resolve("rbacService");
|
|
55
57
|
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
@@ -59,10 +61,30 @@ async function GET(req) {
|
|
|
59
61
|
if (!allowed) return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
|
60
62
|
const url = new URL(req.url);
|
|
61
63
|
const orgFilter = url.searchParams.get("organizationId");
|
|
64
|
+
if (orgFilter && !await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
65
|
+
tenantId: auth.tenantId,
|
|
66
|
+
organizationId: orgFilter
|
|
67
|
+
})) {
|
|
68
|
+
return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
|
69
|
+
}
|
|
62
70
|
const em = container.resolve("em");
|
|
63
71
|
const where = { tenantId: auth.tenantId };
|
|
64
|
-
if (orgFilter)
|
|
65
|
-
|
|
72
|
+
if (orgFilter) {
|
|
73
|
+
where.organizationId = orgFilter;
|
|
74
|
+
} else {
|
|
75
|
+
const featureAccess = await rbac.resolveFeatureOrganizationAccess(auth.sub, [FEATURE], { tenantId: auth.tenantId });
|
|
76
|
+
if (!featureAccess.unrestricted) {
|
|
77
|
+
const organizations = await findWithDecryption(em, Organization, {
|
|
78
|
+
tenant: auth.tenantId,
|
|
79
|
+
deletedAt: null
|
|
80
|
+
}, { fields: ["id", "ancestorIds"] }, { tenantId: auth.tenantId });
|
|
81
|
+
where.organizationId = { $in: featureAccess.filterOrganizationIds(organizations) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const records = await findWithDecryption(em, DomainMapping, where, { orderBy: { createdAt: "desc" } }, {
|
|
85
|
+
tenantId: auth.tenantId,
|
|
86
|
+
organizationId: orgFilter
|
|
87
|
+
});
|
|
66
88
|
return NextResponse.json({
|
|
67
89
|
ok: true,
|
|
68
90
|
domainMappings: records.map(serializeRecord),
|
|
@@ -77,11 +99,6 @@ async function POST(req) {
|
|
|
77
99
|
if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
|
78
100
|
const container = await createRequestContainer();
|
|
79
101
|
const rbac = container.resolve("rbacService");
|
|
80
|
-
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
81
|
-
tenantId: auth.tenantId,
|
|
82
|
-
organizationId: auth.orgId
|
|
83
|
-
});
|
|
84
|
-
if (!allowed) return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
|
85
102
|
const body = await readJsonSafe(req, {});
|
|
86
103
|
const parsed = registerDomainSchema.safeParse(body);
|
|
87
104
|
if (!parsed.success) {
|
|
@@ -90,6 +107,11 @@ async function POST(req) {
|
|
|
90
107
|
{ status: 400 }
|
|
91
108
|
);
|
|
92
109
|
}
|
|
110
|
+
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
111
|
+
tenantId: auth.tenantId,
|
|
112
|
+
organizationId: parsed.data.organizationId
|
|
113
|
+
});
|
|
114
|
+
if (!allowed) return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
|
93
115
|
const guardResult = await validateCrudMutationGuard(container, {
|
|
94
116
|
tenantId: auth.tenantId,
|
|
95
117
|
organizationId: parsed.data.organizationId,
|
|
@@ -149,17 +171,17 @@ async function DELETE(req) {
|
|
|
149
171
|
if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
|
150
172
|
const container = await createRequestContainer();
|
|
151
173
|
const rbac = container.resolve("rbacService");
|
|
152
|
-
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
153
|
-
tenantId: auth.tenantId,
|
|
154
|
-
organizationId: auth.orgId
|
|
155
|
-
});
|
|
156
|
-
if (!allowed) return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
|
157
174
|
const url = new URL(req.url);
|
|
158
175
|
const id = url.searchParams.get("id");
|
|
159
176
|
if (!id) return NextResponse.json({ ok: false, error: "id is required" }, { status: 400 });
|
|
160
177
|
const service = container.resolve("domainMappingService");
|
|
161
178
|
const existing = await service.findById(id, { tenantId: auth.tenantId });
|
|
162
179
|
if (!existing) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
|
180
|
+
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
181
|
+
tenantId: auth.tenantId,
|
|
182
|
+
organizationId: existing.organizationId
|
|
183
|
+
});
|
|
184
|
+
if (!allowed) return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
|
163
185
|
const guardResult = await validateCrudMutationGuard(container, {
|
|
164
186
|
tenantId: auth.tenantId,
|
|
165
187
|
organizationId: existing.organizationId,
|
|
@@ -213,7 +235,7 @@ const openApi = {
|
|
|
213
235
|
methods: {
|
|
214
236
|
GET: {
|
|
215
237
|
summary: "List domain mappings",
|
|
216
|
-
description: "Returns
|
|
238
|
+
description: "Returns custom-domain mappings for authorized organizations in the current tenant, optionally filtered by organization.",
|
|
217
239
|
responses: [
|
|
218
240
|
{
|
|
219
241
|
status: 200,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/admin/domain-mappings.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { registerDomainSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n DomainMappingService,\n DomainMappingOrgScopeError,\n type ResolveResult,\n} from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\nimport { DomainMapping } from '@open-mercato/core/modules/customer_accounts/data/entities'\n\nconst FEATURE = 'customer_accounts.domain.manage'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n POST: { requireAuth: true, requireFeatures: [FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nfunction isUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) return true\n if (!error || typeof error !== 'object') return false\n const code = (error as { code?: string }).code\n if (code === '23505') return true\n const messageRaw = (error as { message?: string }).message\n const message = typeof messageRaw === 'string' ? messageRaw : ''\n return message.toLowerCase().includes('duplicate key')\n}\n\nfunction serializeRecord(record: DomainMapping) {\n return {\n id: record.id,\n hostname: record.hostname,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n provider: record.provider,\n status: record.status,\n verifiedAt: record.verifiedAt?.toISOString() ?? null,\n lastDnsCheckAt: record.lastDnsCheckAt?.toISOString() ?? null,\n dnsFailureReason: record.dnsFailureReason ?? null,\n tlsFailureReason: record.tlsFailureReason ?? null,\n tlsRetryCount: record.tlsRetryCount,\n cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,\n aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt?.toISOString() ?? null,\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const url = new URL(req.url)\n const orgFilter = url.searchParams.get('organizationId')\n\n const em = container.resolve('em') as EntityManager\n const where: Record<string, unknown> = { tenantId: auth.tenantId }\n if (orgFilter) where.organizationId = orgFilter\n const records = await em.find(DomainMapping, where as never, { orderBy: { createdAt: 'desc' } })\n\n return NextResponse.json({\n ok: true,\n domainMappings: records.map(serializeRecord),\n config: {\n cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,\n aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,\n },\n })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const body = await readJsonSafe(req, {})\n const parsed = registerDomainSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { ok: false, error: 'Invalid request', issues: parsed.error.flatten() },\n { status: 400 },\n )\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: parsed.data.organizationId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as unknown as Record<string, unknown>,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const service = container.resolve('domainMappingService') as DomainMappingService\n\n let entity: DomainMapping\n try {\n // service.register normalizes hostname internally \u2014 guards may have\n // returned modifiedPayload but the typed runner doesn't expose it,\n // so we rely on the service for the canonical form.\n entity = await service.register({\n hostname: parsed.data.hostname,\n organizationId: parsed.data.organizationId,\n tenantId: auth.tenantId,\n replacesDomainId: parsed.data.replacesDomainId,\n })\n } catch (err: unknown) {\n if (isUniqueViolation(err)) {\n return NextResponse.json(\n { ok: false, error: 'This domain is not available. Please choose a different hostname.' },\n { status: 409 },\n )\n }\n if (err instanceof DomainMappingOrgScopeError) {\n return NextResponse.json(\n { ok: false, error: 'Organization was not found in the current tenant.' },\n { status: 400 },\n )\n }\n const message = err instanceof Error ? err.message : 'Failed to register domain'\n return NextResponse.json({ ok: false, error: message }, { status: 500 })\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: entity.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, domainMapping: serializeRecord(entity) }, { status: 201 })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n if (!id) return NextResponse.json({ ok: false, error: 'id is required' }, { status: 400 })\n\n const service = container.resolve('domainMappingService') as DomainMappingService\n const existing = await service.findById(id, { tenantId: auth.tenantId })\n if (!existing) return NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n await service.remove(id, { tenantId: auth.tenantId })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\nconst domainMappingSchema = z.object({\n id: z.string().uuid(),\n hostname: z.string(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n provider: z.literal('traefik'),\n status: z.enum(['pending', 'verified', 'active', 'dns_failed', 'tls_failed']),\n verifiedAt: z.string().nullable(),\n lastDnsCheckAt: z.string().nullable(),\n dnsFailureReason: z.string().nullable(),\n tlsFailureReason: z.string().nullable(),\n tlsRetryCount: z.number().int().nonnegative(),\n cnameTarget: z.string().nullable(),\n aRecordTarget: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'Custom portal domain mappings (admin)',\n methods: {\n GET: {\n summary: 'List domain mappings',\n description: 'Returns all custom-domain mappings for the current tenant, optionally filtered by organization.',\n responses: [\n {\n status: 200,\n description: 'OK',\n schema: z.object({\n ok: z.literal(true),\n domainMappings: z.array(domainMappingSchema),\n config: z.object({\n cnameTarget: z.string().nullable(),\n aRecordTarget: z.string().nullable(),\n }),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Register a custom domain',\n description: 'Registers a new custom domain mapping for an organization. Verifies via DNS asynchronously.',\n requestBody: {\n contentType: 'application/json',\n schema: z.object({\n hostname: z.string(),\n organizationId: z.string().uuid(),\n replacesDomainId: z.string().uuid().optional(),\n }),\n },\n responses: [\n {\n status: 201,\n description: 'Created',\n schema: z.object({ ok: z.literal(true), domainMapping: domainMappingSchema }),\n },\n ],\n errors: [\n { status: 400, description: 'Validation error', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 409, description: 'Conflict', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Remove a custom domain',\n description: 'Removes the domain mapping identified by ?id=. Cache and Traefik routing drain within TTL.',\n responses: [{ status: 200, description: 'OK', schema: z.object({ ok: z.literal(true) }) }],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n\n// Re-export resolve type so tests can import without indirect lookup\nexport type { ResolveResult }\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { UniqueConstraintViolationException, type FilterQuery } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { registerDomainSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport {\n DomainMappingService,\n DomainMappingOrgScopeError,\n type ResolveResult,\n} from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\nimport { DomainMapping } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\n\nconst FEATURE = 'customer_accounts.domain.manage'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [FEATURE] },\n POST: { requireAuth: true, requireFeatures: [FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [FEATURE] },\n}\n\nfunction isUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) return true\n if (!error || typeof error !== 'object') return false\n const code = (error as { code?: string }).code\n if (code === '23505') return true\n const messageRaw = (error as { message?: string }).message\n const message = typeof messageRaw === 'string' ? messageRaw : ''\n return message.toLowerCase().includes('duplicate key')\n}\n\nfunction serializeRecord(record: DomainMapping) {\n return {\n id: record.id,\n hostname: record.hostname,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n provider: record.provider,\n status: record.status,\n verifiedAt: record.verifiedAt?.toISOString() ?? null,\n lastDnsCheckAt: record.lastDnsCheckAt?.toISOString() ?? null,\n dnsFailureReason: record.dnsFailureReason ?? null,\n tlsFailureReason: record.tlsFailureReason ?? null,\n tlsRetryCount: record.tlsRetryCount,\n cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,\n aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt?.toISOString() ?? null,\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const url = new URL(req.url)\n const orgFilter = url.searchParams.get('organizationId')\n if (orgFilter && !await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: orgFilter,\n })) {\n return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n }\n\n const em = container.resolve('em') as EntityManager\n const where: FilterQuery<DomainMapping> = { tenantId: auth.tenantId }\n if (orgFilter) {\n where.organizationId = orgFilter\n } else {\n const featureAccess = await rbac.resolveFeatureOrganizationAccess(auth.sub, [FEATURE], { tenantId: auth.tenantId })\n if (!featureAccess.unrestricted) {\n const organizations = await findWithDecryption(em, Organization, {\n tenant: auth.tenantId,\n deletedAt: null,\n }, { fields: ['id', 'ancestorIds'] }, { tenantId: auth.tenantId })\n where.organizationId = { $in: featureAccess.filterOrganizationIds(organizations) }\n }\n }\n const records = await findWithDecryption(em, DomainMapping, where, { orderBy: { createdAt: 'desc' } }, {\n tenantId: auth.tenantId,\n organizationId: orgFilter,\n })\n\n return NextResponse.json({\n ok: true,\n domainMappings: records.map(serializeRecord),\n config: {\n cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,\n aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,\n },\n })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n\n const body = await readJsonSafe(req, {})\n const parsed = registerDomainSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { ok: false, error: 'Invalid request', issues: parsed.error.flatten() },\n { status: 400 },\n )\n }\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: parsed.data.organizationId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as unknown as Record<string, unknown>,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const service = container.resolve('domainMappingService') as DomainMappingService\n\n let entity: DomainMapping\n try {\n // service.register normalizes hostname internally \u2014 guards may have\n // returned modifiedPayload but the typed runner doesn't expose it,\n // so we rely on the service for the canonical form.\n entity = await service.register({\n hostname: parsed.data.hostname,\n organizationId: parsed.data.organizationId,\n tenantId: auth.tenantId,\n replacesDomainId: parsed.data.replacesDomainId,\n })\n } catch (err: unknown) {\n if (isUniqueViolation(err)) {\n return NextResponse.json(\n { ok: false, error: 'This domain is not available. Please choose a different hostname.' },\n { status: 409 },\n )\n }\n if (err instanceof DomainMappingOrgScopeError) {\n return NextResponse.json(\n { ok: false, error: 'Organization was not found in the current tenant.' },\n { status: 400 },\n )\n }\n const message = err instanceof Error ? err.message : 'Failed to register domain'\n return NextResponse.json({ ok: false, error: message }, { status: 500 })\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: parsed.data.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: entity.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, domainMapping: serializeRecord(entity) }, { status: 201 })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const rbac = container.resolve('rbacService') as RbacService\n\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n if (!id) return NextResponse.json({ ok: false, error: 'id is required' }, { status: 400 })\n\n const service = container.resolve('domainMappingService') as DomainMappingService\n const existing = await service.findById(id, { tenantId: auth.tenantId })\n if (!existing) return NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 })\n const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n })\n if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n await service.remove(id, { tenantId: auth.tenantId })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: existing.organizationId,\n userId: auth.sub,\n resourceKind: 'customer_accounts.domain_mapping',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\nconst domainMappingSchema = z.object({\n id: z.string().uuid(),\n hostname: z.string(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n provider: z.literal('traefik'),\n status: z.enum(['pending', 'verified', 'active', 'dns_failed', 'tls_failed']),\n verifiedAt: z.string().nullable(),\n lastDnsCheckAt: z.string().nullable(),\n dnsFailureReason: z.string().nullable(),\n tlsFailureReason: z.string().nullable(),\n tlsRetryCount: z.number().int().nonnegative(),\n cnameTarget: z.string().nullable(),\n aRecordTarget: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerAccounts',\n summary: 'Custom portal domain mappings (admin)',\n methods: {\n GET: {\n summary: 'List domain mappings',\n description: 'Returns custom-domain mappings for authorized organizations in the current tenant, optionally filtered by organization.',\n responses: [\n {\n status: 200,\n description: 'OK',\n schema: z.object({\n ok: z.literal(true),\n domainMappings: z.array(domainMappingSchema),\n config: z.object({\n cnameTarget: z.string().nullable(),\n aRecordTarget: z.string().nullable(),\n }),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Register a custom domain',\n description: 'Registers a new custom domain mapping for an organization. Verifies via DNS asynchronously.',\n requestBody: {\n contentType: 'application/json',\n schema: z.object({\n hostname: z.string(),\n organizationId: z.string().uuid(),\n replacesDomainId: z.string().uuid().optional(),\n }),\n },\n responses: [\n {\n status: 201,\n description: 'Created',\n schema: z.object({ ok: z.literal(true), domainMapping: domainMappingSchema }),\n },\n ],\n errors: [\n { status: 400, description: 'Validation error', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n { status: 409, description: 'Conflict', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Remove a custom domain',\n description: 'Removes the domain mapping identified by ?id=. Cache and Traefik routing drain within TTL.',\n responses: [{ status: 200, description: 'OK', schema: z.object({ ok: z.literal(true) }) }],\n errors: [\n { status: 400, description: 'Bad request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n\n// Re-export resolve type so tests can import without indirect lookup\nexport type { ResolveResult }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext";
|
|
20
20
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
21
21
|
import { isCrossSiteBrowserRequest } from "@open-mercato/shared/lib/url";
|
|
22
|
+
import { isCustomerTenantActive } from "@open-mercato/core/modules/customer_accounts/lib/tenantEligibility";
|
|
22
23
|
const metadata = { requireAuth: false };
|
|
23
24
|
const TIMING_EQUALIZATION_HASH = "$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i";
|
|
24
25
|
function reportLoginEventError(error) {
|
|
@@ -64,7 +65,7 @@ async function POST(req) {
|
|
|
64
65
|
const customerSessionService = container.resolve("customerSessionService");
|
|
65
66
|
const customerRbacService = container.resolve("customerRbacService");
|
|
66
67
|
const user = await customerUserService.findByEmail(email, tenantId);
|
|
67
|
-
if (!user || !user.passwordHash) {
|
|
68
|
+
if (!user || !user.passwordHash || !await isCustomerTenantActive(container.resolve("em"), tenantId)) {
|
|
68
69
|
await bcryptCompare(password, TIMING_EQUALIZATION_HASH);
|
|
69
70
|
void emitCustomerAccountsEvent("customer_accounts.login.failed", { email, reason: "invalid_credentials", tenantId }).catch(reportLoginEventError);
|
|
70
71
|
return NextResponse.json({ ok: false, error: "Invalid email or password" }, { status: 401 });
|
|
@@ -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 { 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'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { isCrossSiteBrowserRequest } from '@open-mercato/shared/lib/url'\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 if (isCrossSiteBrowserRequest(req)) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({ ok: false, error: translate('customer_accounts.errors.crossSiteRequest', 'This sign-in request did not come from this application. Open the sign-in page and try again.') }, { status: 403 })\n }\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = 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: 403, description: 'Request was submitted cross-site', 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": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,WAAW,qBAAqB;AACzC,SAAS,SAAS;AAElB,SAAS,mBAAmB;AAC5B,SAAS,8BAA8B;AAIvC,SAAS,iCAAiC;AAC1C,SAAS,2BAA2B;AACpC,SAAS,4BAA4B;AACrC,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC,SAAS,iCAAiC;
|
|
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'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { isCrossSiteBrowserRequest } from '@open-mercato/shared/lib/url'\nimport { isCustomerTenantActive } from '@open-mercato/core/modules/customer_accounts/lib/tenantEligibility'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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 if (isCrossSiteBrowserRequest(req)) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({ ok: false, error: translate('customer_accounts.errors.crossSiteRequest', 'This sign-in request did not come from this application. Open the sign-in page and try again.') }, { status: 403 })\n }\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = 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 || !(await isCustomerTenantActive(container.resolve('em') as EntityManager, tenantId))) {\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: 403, description: 'Request was submitted cross-site', 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
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { verifyAudienceJwt, verifyJwt } from "@open-mercato/shared/lib/auth/jwt";
|
|
3
3
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
4
|
import { CUSTOMER_JWT_AUDIENCE } from "@open-mercato/core/modules/customer_accounts/services/customerSessionService";
|
|
5
|
+
import { isCustomerTenantActive } from "./tenantEligibility.js";
|
|
5
6
|
async function resolveSessionService() {
|
|
6
7
|
const [{ createRequestContainer }, { CustomerSessionService }] = await Promise.all([
|
|
7
8
|
import("@open-mercato/shared/lib/di/container"),
|
|
@@ -54,6 +55,7 @@ async function validateUserState(sub, tenantId, orgId, iat) {
|
|
|
54
55
|
if (user.sessionsRevokedAt && typeof iat === "number" && iat * 1e3 < user.sessionsRevokedAt.getTime()) {
|
|
55
56
|
return { valid: false };
|
|
56
57
|
}
|
|
58
|
+
if (!await isCustomerTenantActive(em, tenantId)) return { valid: false };
|
|
57
59
|
const { CustomerRbacService } = await import("@open-mercato/core/modules/customer_accounts/services/customerRbacService");
|
|
58
60
|
const rbac = container.resolve("customerRbacService");
|
|
59
61
|
const acl = await rbac.loadAcl(sub, { tenantId, organizationId: orgId });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/lib/customerAuth.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { verifyAudienceJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CUSTOMER_JWT_AUDIENCE } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\n\nexport interface CustomerAuthContext {\n sub: string\n sid: string\n type: 'customer'\n tenantId: string\n orgId: string\n email: string\n displayName: string\n customerEntityId?: string | null\n personEntityId?: string | null\n resolvedFeatures: string[]\n isPortalAdmin?: boolean\n}\n\nasync function resolveSessionService(): Promise<\n InstanceType<typeof import('@open-mercato/core/modules/customer_accounts/services/customerSessionService').CustomerSessionService>\n> {\n const [{ createRequestContainer }, { CustomerSessionService }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/customer_accounts/services/customerSessionService'),\n ])\n const container = await createRequestContainer()\n return container.resolve('customerSessionService') as InstanceType<typeof CustomerSessionService>\n}\n\nasync function assertSessionStillActive(input: {\n sessionId: string\n userId: string\n tenantId: string\n organizationId: string\n}): Promise<boolean> {\n try {\n const service = await resolveSessionService()\n const session = await service.findActiveSessionForClaims(input)\n return session !== null\n } catch {\n // Fail closed: if we cannot verify the session, treat the token as revoked to prevent\n // replay of leaked JWTs when the backend is partially degraded.\n return false\n }\n}\n\n/**\n * Liveness re-check for a legacy token that carries no `sid` claim. It cannot name the session it\n * was issued for, so the check falls back to the strongest available statement: the customer must\n * still be signed in somewhere. Once every session is revoked \u2014 logout, per-device revoke, admin\n * action \u2014 the token stops authenticating instead of surviving until its own expiry.\n */\nasync function assertUserStillHasActiveSession(input: {\n userId: string\n tenantId: string\n organizationId: string\n}): Promise<boolean> {\n try {\n const service = await resolveSessionService()\n return await service.hasActiveSessionForUser(input)\n } catch {\n // Same fail-closed contract as the sid-bound check above.\n return false\n }\n}\n\nexport function readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nexport type UserValidationResult =\n | { valid: false }\n | { valid: true; resolvedFeatures: string[]; isPortalAdmin: boolean }\n\nexport async function validateUserState(\n sub: string,\n tenantId: string,\n orgId: string,\n iat: unknown,\n): Promise<UserValidationResult> {\n const [{ createRequestContainer }, { CustomerUser }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/customer_accounts/data/entities'),\n ])\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const user = await findOneWithDecryption(em, CustomerUser, { id: sub }, {\n fields: ['sessionsRevokedAt', 'deletedAt', 'isActive'],\n })\n if (!user) return { valid: false }\n if (user.deletedAt) return { valid: false }\n if (!user.isActive) return { valid: false }\n if (user.sessionsRevokedAt && typeof iat === 'number' && iat * 1000 < user.sessionsRevokedAt.getTime()) {\n return { valid: false }\n }\n\n const { CustomerRbacService } = await import(\n '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\n )\n const rbac = container.resolve('customerRbacService') as InstanceType<typeof CustomerRbacService>\n const acl = await rbac.loadAcl(sub, { tenantId, organizationId: orgId })\n const resolvedFeatures = await rbac.getEffectiveFeatures(sub, {\n tenantId,\n organizationId: orgId,\n })\n return { valid: true, resolvedFeatures, isPortalAdmin: acl.isPortalAdmin }\n}\n\nexport async function getCustomerAuthFromRequest(req: Request): Promise<CustomerAuthContext | null> {\n const cookieHeader = req.headers.get('cookie') || ''\n const authHeader = (req.headers.get('authorization') || '').trim()\n\n let token: string | undefined\n if (authHeader.toLowerCase().startsWith('bearer ')) {\n token = authHeader.slice(7).trim()\n }\n if (!token) {\n const cookieValue = readCookieFromHeader(cookieHeader, 'customer_auth_token')\n if (cookieValue) {\n try {\n token = decodeURIComponent(cookieValue)\n } catch {\n // Malformed percent-encoding; use raw value\n token = cookieValue\n }\n }\n }\n if (!token) return null\n\n try {\n let payload = verifyAudienceJwt(CUSTOMER_JWT_AUDIENCE, token) as Record<string, unknown> | null\n // Legacy fallback: accept a pre-migration customer token signed with the raw JWT_SECRET, but\n // only while `verifyJwt` itself still considers it legacy \u2014 it owns the grace window (token\n // `iat` vs JWT_LEGACY_GRACE_MINUTES / JWT_LEGACY_CUTOVER_AT) and marks the payload. Trusting\n // the bare return value would also let a staff-audience token through this branch, because\n // the default `verifyJwt` path verifies against the staff-derived key.\n if (!payload) {\n const legacyPayload = verifyJwt(token) as Record<string, unknown> | null\n if (legacyPayload && legacyPayload._legacyToken === true) payload = legacyPayload\n }\n if (!payload) return null\n if (payload.type !== 'customer') return null\n const sid = typeof payload.sid === 'string' ? payload.sid : ''\n if (!sid && payload._legacyToken !== true) return null\n const userId = String(payload.sub)\n const tenantId = String(payload.tenantId)\n const organizationId = String(payload.orgId)\n // Every accepted token is re-checked for liveness, exactly like the SSR path does \u2014 a token\n // without a `sid` is checked against the customer's remaining sessions rather than skipped,\n // so session revocation is never structurally unreachable on the API path.\n const stillActive = sid\n ? await assertSessionStillActive({\n sessionId: sid,\n userId,\n tenantId,\n organizationId,\n })\n : await assertUserStillHasActiveSession({ userId, tenantId, organizationId })\n if (!stillActive) return null\n\n const userState = await validateUserState(\n userId,\n tenantId,\n organizationId,\n payload.iat,\n )\n if (!userState.valid) return null\n\n return {\n sub: userId,\n sid,\n type: 'customer',\n tenantId,\n orgId: organizationId,\n email: String(payload.email || ''),\n displayName: String(payload.displayName || ''),\n customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,\n personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,\n resolvedFeatures: userState.resolvedFeatures,\n isPortalAdmin: userState.isPortalAdmin,\n }\n } catch {\n // Invalid or expired JWT \u2014 treat as unauthenticated\n return null\n }\n}\n\nexport async function requireCustomerAuth(req: Request): Promise<CustomerAuthContext> {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n throw NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n return auth\n}\n\nexport async function requireCustomerFeature(\n auth: CustomerAuthContext,\n features: string[],\n rbac: CustomerRbacService,\n): Promise<void> {\n if (!features.length) return\n const ok = await rbac.userHasAllFeatures(\n auth.sub,\n features,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!ok) {\n throw NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB,iBAAiB;AAE7C,SAAS,6BAA6B;AACtC,SAAS,6BAA6B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { verifyAudienceJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CUSTOMER_JWT_AUDIENCE } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { isCustomerTenantActive } from './tenantEligibility'\n\nexport interface CustomerAuthContext {\n sub: string\n sid: string\n type: 'customer'\n tenantId: string\n orgId: string\n email: string\n displayName: string\n customerEntityId?: string | null\n personEntityId?: string | null\n resolvedFeatures: string[]\n isPortalAdmin?: boolean\n}\n\nasync function resolveSessionService(): Promise<\n InstanceType<typeof import('@open-mercato/core/modules/customer_accounts/services/customerSessionService').CustomerSessionService>\n> {\n const [{ createRequestContainer }, { CustomerSessionService }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/customer_accounts/services/customerSessionService'),\n ])\n const container = await createRequestContainer()\n return container.resolve('customerSessionService') as InstanceType<typeof CustomerSessionService>\n}\n\nasync function assertSessionStillActive(input: {\n sessionId: string\n userId: string\n tenantId: string\n organizationId: string\n}): Promise<boolean> {\n try {\n const service = await resolveSessionService()\n const session = await service.findActiveSessionForClaims(input)\n return session !== null\n } catch {\n // Fail closed: if we cannot verify the session, treat the token as revoked to prevent\n // replay of leaked JWTs when the backend is partially degraded.\n return false\n }\n}\n\n/**\n * Liveness re-check for a legacy token that carries no `sid` claim. It cannot name the session it\n * was issued for, so the check falls back to the strongest available statement: the customer must\n * still be signed in somewhere. Once every session is revoked \u2014 logout, per-device revoke, admin\n * action \u2014 the token stops authenticating instead of surviving until its own expiry.\n */\nasync function assertUserStillHasActiveSession(input: {\n userId: string\n tenantId: string\n organizationId: string\n}): Promise<boolean> {\n try {\n const service = await resolveSessionService()\n return await service.hasActiveSessionForUser(input)\n } catch {\n // Same fail-closed contract as the sid-bound check above.\n return false\n }\n}\n\nexport function readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nexport type UserValidationResult =\n | { valid: false }\n | { valid: true; resolvedFeatures: string[]; isPortalAdmin: boolean }\n\nexport async function validateUserState(\n sub: string,\n tenantId: string,\n orgId: string,\n iat: unknown,\n): Promise<UserValidationResult> {\n const [{ createRequestContainer }, { CustomerUser }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/customer_accounts/data/entities'),\n ])\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const user = await findOneWithDecryption(em, CustomerUser, { id: sub }, {\n fields: ['sessionsRevokedAt', 'deletedAt', 'isActive'],\n })\n if (!user) return { valid: false }\n if (user.deletedAt) return { valid: false }\n if (!user.isActive) return { valid: false }\n if (user.sessionsRevokedAt && typeof iat === 'number' && iat * 1000 < user.sessionsRevokedAt.getTime()) {\n return { valid: false }\n }\n if (!(await isCustomerTenantActive(em, tenantId))) return { valid: false }\n\n const { CustomerRbacService } = await import(\n '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\n )\n const rbac = container.resolve('customerRbacService') as InstanceType<typeof CustomerRbacService>\n const acl = await rbac.loadAcl(sub, { tenantId, organizationId: orgId })\n const resolvedFeatures = await rbac.getEffectiveFeatures(sub, {\n tenantId,\n organizationId: orgId,\n })\n return { valid: true, resolvedFeatures, isPortalAdmin: acl.isPortalAdmin }\n}\n\nexport async function getCustomerAuthFromRequest(req: Request): Promise<CustomerAuthContext | null> {\n const cookieHeader = req.headers.get('cookie') || ''\n const authHeader = (req.headers.get('authorization') || '').trim()\n\n let token: string | undefined\n if (authHeader.toLowerCase().startsWith('bearer ')) {\n token = authHeader.slice(7).trim()\n }\n if (!token) {\n const cookieValue = readCookieFromHeader(cookieHeader, 'customer_auth_token')\n if (cookieValue) {\n try {\n token = decodeURIComponent(cookieValue)\n } catch {\n // Malformed percent-encoding; use raw value\n token = cookieValue\n }\n }\n }\n if (!token) return null\n\n try {\n let payload = verifyAudienceJwt(CUSTOMER_JWT_AUDIENCE, token) as Record<string, unknown> | null\n // Legacy fallback: accept a pre-migration customer token signed with the raw JWT_SECRET, but\n // only while `verifyJwt` itself still considers it legacy \u2014 it owns the grace window (token\n // `iat` vs JWT_LEGACY_GRACE_MINUTES / JWT_LEGACY_CUTOVER_AT) and marks the payload. Trusting\n // the bare return value would also let a staff-audience token through this branch, because\n // the default `verifyJwt` path verifies against the staff-derived key.\n if (!payload) {\n const legacyPayload = verifyJwt(token) as Record<string, unknown> | null\n if (legacyPayload && legacyPayload._legacyToken === true) payload = legacyPayload\n }\n if (!payload) return null\n if (payload.type !== 'customer') return null\n const sid = typeof payload.sid === 'string' ? payload.sid : ''\n if (!sid && payload._legacyToken !== true) return null\n const userId = String(payload.sub)\n const tenantId = String(payload.tenantId)\n const organizationId = String(payload.orgId)\n // Every accepted token is re-checked for liveness, exactly like the SSR path does \u2014 a token\n // without a `sid` is checked against the customer's remaining sessions rather than skipped,\n // so session revocation is never structurally unreachable on the API path.\n const stillActive = sid\n ? await assertSessionStillActive({\n sessionId: sid,\n userId,\n tenantId,\n organizationId,\n })\n : await assertUserStillHasActiveSession({ userId, tenantId, organizationId })\n if (!stillActive) return null\n\n const userState = await validateUserState(\n userId,\n tenantId,\n organizationId,\n payload.iat,\n )\n if (!userState.valid) return null\n\n return {\n sub: userId,\n sid,\n type: 'customer',\n tenantId,\n orgId: organizationId,\n email: String(payload.email || ''),\n displayName: String(payload.displayName || ''),\n customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,\n personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,\n resolvedFeatures: userState.resolvedFeatures,\n isPortalAdmin: userState.isPortalAdmin,\n }\n } catch {\n // Invalid or expired JWT \u2014 treat as unauthenticated\n return null\n }\n}\n\nexport async function requireCustomerAuth(req: Request): Promise<CustomerAuthContext> {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n throw NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n return auth\n}\n\nexport async function requireCustomerFeature(\n auth: CustomerAuthContext,\n features: string[],\n rbac: CustomerRbacService,\n): Promise<void> {\n if (!features.length) return\n const ok = await rbac.userHasAllFeatures(\n auth.sub,\n features,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!ok) {\n throw NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Tenant } from "@open-mercato/core/modules/directory/data/entities";
|
|
2
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
async function isCustomerTenantActive(em, tenantId) {
|
|
4
|
+
if (!tenantId) return false;
|
|
5
|
+
const tenant = await findOneWithDecryption(
|
|
6
|
+
em,
|
|
7
|
+
Tenant,
|
|
8
|
+
{ id: tenantId, isActive: true, deletedAt: null },
|
|
9
|
+
{ refresh: true }
|
|
10
|
+
);
|
|
11
|
+
return Boolean(tenant);
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
isCustomerTenantActive
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=tenantEligibility.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customer_accounts/lib/tenantEligibility.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nexport async function isCustomerTenantActive(em: EntityManager, tenantId: string): Promise<boolean> {\n if (!tenantId) return false\n const tenant = await findOneWithDecryption(\n em,\n Tenant,\n { id: tenantId, isActive: true, deletedAt: null },\n { refresh: true },\n )\n return Boolean(tenant)\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,cAAc;AACvB,SAAS,6BAA6B;AAEtC,eAAsB,uBAAuB,IAAmB,UAAoC;AAClG,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA,EAAE,IAAI,UAAU,UAAU,MAAM,WAAW,KAAK;AAAA,IAChD,EAAE,SAAS,KAAK;AAAA,EAClB;AACA,SAAO,QAAQ,MAAM;AACvB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { generateSecureToken, hashToken } from "@open-mercato/core/modules/customer_accounts/lib/tokenGenerator";
|
|
11
11
|
import { hashForLookup, lookupHashCandidates } from "@open-mercato/shared/lib/encryption/aes";
|
|
12
12
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
13
|
+
import { isCustomerTenantActive } from "@open-mercato/core/modules/customer_accounts/lib/tenantEligibility";
|
|
13
14
|
const BCRYPT_COST = 10;
|
|
14
15
|
const INVITATION_TTL_MS = 72 * 60 * 60 * 1e3;
|
|
15
16
|
const CUSTOMER_INVITATION_EMAIL_CONFLICT_MARKER = /* @__PURE__ */ Symbol.for("@open-mercato/CustomerInvitationEmailConflictError");
|
|
@@ -132,6 +133,7 @@ class CustomerInvitationService {
|
|
|
132
133
|
async acceptInvitation(token, password, displayName) {
|
|
133
134
|
const invitation = await this.findByToken(token);
|
|
134
135
|
if (!invitation) return null;
|
|
136
|
+
if (!await isCustomerTenantActive(this.em, invitation.tenantId)) return null;
|
|
135
137
|
const emailHash = hashForLookup(invitation.email);
|
|
136
138
|
const existingActiveUser = await findOneWithDecryption(
|
|
137
139
|
this.em,
|