@open-mercato/core 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3278.1.a4d26475e4
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 +2 -2
- package/dist/generated/entities/domain_mapping/index.js +31 -0
- package/dist/generated/entities/domain_mapping/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +16 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +18 -17
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/customer_accounts/acl.js +2 -1
- package/dist/modules/customer_accounts/acl.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-check.js +70 -0
- package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
- package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
- package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
- package/dist/modules/customer_accounts/api/login.js +14 -3
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
- package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
- package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
- package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
- package/dist/modules/customer_accounts/api/signup.js +32 -5
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/data/enrichers.js +78 -12
- package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +85 -2
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/guards.js +130 -0
- package/dist/modules/customer_accounts/data/guards.js.map +7 -0
- package/dist/modules/customer_accounts/data/validators.js +19 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/di.js +23 -1
- package/dist/modules/customer_accounts/di.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +9 -2
- package/dist/modules/customer_accounts/events.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
- package/dist/modules/customer_accounts/lib/hostname.js +55 -0
- package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
- package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
- package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
- package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
- package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
- package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
- package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
- package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/notifications.client.js +76 -0
- package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
- package/dist/modules/customer_accounts/notifications.js +76 -0
- package/dist/modules/customer_accounts/notifications.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
- package/dist/modules/customer_accounts/setup.js +48 -1
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
- package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
- package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
- package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
- package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
- package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
- package/dist/modules/customers/api/companies/route.js +4 -9
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +4 -9
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +4 -9
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +0 -12
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +361 -284
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +378 -473
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +368 -286
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
- package/dist/modules/customers/widgets/injection-table.js +6 -0
- package/dist/modules/customers/widgets/injection-table.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +16 -2
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +31 -8
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/generated/entities/domain_mapping/index.ts +14 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +16 -0
- package/package.json +5 -5
- package/src/helpers/integration/salesUi.ts +27 -23
- package/src/modules/customer_accounts/acl.ts +1 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
- package/src/modules/customer_accounts/api/domain-check.ts +83 -0
- package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
- package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
- package/src/modules/customer_accounts/api/login.ts +14 -3
- package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
- package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
- package/src/modules/customer_accounts/api/signup.ts +36 -5
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
- package/src/modules/customer_accounts/data/enrichers.ts +95 -12
- package/src/modules/customer_accounts/data/entities.ts +80 -1
- package/src/modules/customer_accounts/data/guards.ts +155 -0
- package/src/modules/customer_accounts/data/validators.ts +26 -0
- package/src/modules/customer_accounts/di.ts +35 -1
- package/src/modules/customer_accounts/events.ts +8 -1
- package/src/modules/customer_accounts/i18n/de.json +78 -0
- package/src/modules/customer_accounts/i18n/en.json +78 -0
- package/src/modules/customer_accounts/i18n/es.json +78 -0
- package/src/modules/customer_accounts/i18n/pl.json +78 -0
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
- package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
- package/src/modules/customer_accounts/lib/hostname.ts +76 -0
- package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
- package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
- package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
- package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
- package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
- package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
- package/src/modules/customer_accounts/notifications.client.ts +76 -0
- package/src/modules/customer_accounts/notifications.ts +76 -0
- package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
- package/src/modules/customer_accounts/setup.ts +81 -1
- package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
- package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
- package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
- package/src/modules/customers/api/companies/route.ts +4 -9
- package/src/modules/customers/api/deals/route.ts +4 -9
- package/src/modules/customers/api/people/route.ts +4 -9
- package/src/modules/customers/api/utils.ts +0 -14
- package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
- package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
- package/src/modules/customers/backend/customers/people/page.tsx +331 -240
- package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
- package/src/modules/customers/i18n/de.json +31 -0
- package/src/modules/customers/i18n/en.json +31 -0
- package/src/modules/customers/i18n/es.json +31 -0
- package/src/modules/customers/i18n/pl.json +31 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
- package/src/modules/customers/widgets/injection-table.ts +6 -0
- package/src/modules/perspectives/services/perspectiveService.ts +30 -2
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
- package/src/modules/query_index/lib/engine.ts +46 -8
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { UniqueConstraintViolationException } from '@mikro-orm/core'
|
|
4
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
7
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
8
|
+
import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
9
|
+
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
10
|
+
import {
|
|
11
|
+
validateCrudMutationGuard,
|
|
12
|
+
runCrudMutationGuardAfterSuccess,
|
|
13
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
14
|
+
import { registerDomainSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
15
|
+
import {
|
|
16
|
+
DomainMappingService,
|
|
17
|
+
type ResolveResult,
|
|
18
|
+
} from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
|
|
19
|
+
import { DomainMapping } from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
20
|
+
|
|
21
|
+
const FEATURE = 'customer_accounts.domain.manage'
|
|
22
|
+
|
|
23
|
+
export const metadata = {
|
|
24
|
+
GET: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
25
|
+
POST: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
26
|
+
DELETE: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isUniqueViolation(error: unknown): boolean {
|
|
30
|
+
if (error instanceof UniqueConstraintViolationException) return true
|
|
31
|
+
if (!error || typeof error !== 'object') return false
|
|
32
|
+
const code = (error as { code?: string }).code
|
|
33
|
+
if (code === '23505') return true
|
|
34
|
+
const messageRaw = (error as { message?: string }).message
|
|
35
|
+
const message = typeof messageRaw === 'string' ? messageRaw : ''
|
|
36
|
+
return message.toLowerCase().includes('duplicate key')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function serializeRecord(record: DomainMapping) {
|
|
40
|
+
return {
|
|
41
|
+
id: record.id,
|
|
42
|
+
hostname: record.hostname,
|
|
43
|
+
organizationId: record.organizationId,
|
|
44
|
+
tenantId: record.tenantId,
|
|
45
|
+
provider: record.provider,
|
|
46
|
+
status: record.status,
|
|
47
|
+
verifiedAt: record.verifiedAt?.toISOString() ?? null,
|
|
48
|
+
lastDnsCheckAt: record.lastDnsCheckAt?.toISOString() ?? null,
|
|
49
|
+
dnsFailureReason: record.dnsFailureReason ?? null,
|
|
50
|
+
tlsFailureReason: record.tlsFailureReason ?? null,
|
|
51
|
+
tlsRetryCount: record.tlsRetryCount,
|
|
52
|
+
cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,
|
|
53
|
+
aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,
|
|
54
|
+
createdAt: record.createdAt.toISOString(),
|
|
55
|
+
updatedAt: record.updatedAt?.toISOString() ?? null,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function GET(req: Request) {
|
|
60
|
+
const auth = await getAuthFromRequest(req)
|
|
61
|
+
if (!auth) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })
|
|
62
|
+
|
|
63
|
+
const container = await createRequestContainer()
|
|
64
|
+
const rbac = container.resolve('rbacService') as RbacService
|
|
65
|
+
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
66
|
+
tenantId: auth.tenantId,
|
|
67
|
+
organizationId: auth.orgId,
|
|
68
|
+
})
|
|
69
|
+
if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })
|
|
70
|
+
|
|
71
|
+
const url = new URL(req.url)
|
|
72
|
+
const orgFilter = url.searchParams.get('organizationId')
|
|
73
|
+
|
|
74
|
+
const em = container.resolve('em') as EntityManager
|
|
75
|
+
const where: Record<string, unknown> = { tenantId: auth.tenantId }
|
|
76
|
+
if (orgFilter) where.organizationId = orgFilter
|
|
77
|
+
const records = await em.find(DomainMapping, where as never, { orderBy: { createdAt: 'desc' } })
|
|
78
|
+
|
|
79
|
+
return NextResponse.json({
|
|
80
|
+
ok: true,
|
|
81
|
+
domainMappings: records.map(serializeRecord),
|
|
82
|
+
config: {
|
|
83
|
+
cnameTarget: process.env.CUSTOM_DOMAIN_CNAME_TARGET ?? null,
|
|
84
|
+
aRecordTarget: process.env.CUSTOM_DOMAIN_A_RECORD_TARGET ?? null,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function POST(req: Request) {
|
|
90
|
+
const auth = await getAuthFromRequest(req)
|
|
91
|
+
if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })
|
|
92
|
+
|
|
93
|
+
const container = await createRequestContainer()
|
|
94
|
+
const rbac = container.resolve('rbacService') as RbacService
|
|
95
|
+
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
96
|
+
tenantId: auth.tenantId,
|
|
97
|
+
organizationId: auth.orgId,
|
|
98
|
+
})
|
|
99
|
+
if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })
|
|
100
|
+
|
|
101
|
+
const body = await readJsonSafe(req, {})
|
|
102
|
+
const parsed = registerDomainSchema.safeParse(body)
|
|
103
|
+
if (!parsed.success) {
|
|
104
|
+
return NextResponse.json(
|
|
105
|
+
{ ok: false, error: 'Invalid request', issues: parsed.error.flatten() },
|
|
106
|
+
{ status: 400 },
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
111
|
+
tenantId: auth.tenantId,
|
|
112
|
+
organizationId: parsed.data.organizationId,
|
|
113
|
+
userId: auth.sub,
|
|
114
|
+
resourceKind: 'customer_accounts.domain_mapping',
|
|
115
|
+
resourceId: parsed.data.organizationId,
|
|
116
|
+
operation: 'create',
|
|
117
|
+
requestMethod: req.method,
|
|
118
|
+
requestHeaders: req.headers,
|
|
119
|
+
mutationPayload: parsed.data as unknown as Record<string, unknown>,
|
|
120
|
+
})
|
|
121
|
+
if (guardResult && !guardResult.ok) {
|
|
122
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const service = container.resolve('domainMappingService') as DomainMappingService
|
|
126
|
+
|
|
127
|
+
let entity: DomainMapping
|
|
128
|
+
try {
|
|
129
|
+
// service.register normalizes hostname internally — guards may have
|
|
130
|
+
// returned modifiedPayload but the typed runner doesn't expose it,
|
|
131
|
+
// so we rely on the service for the canonical form.
|
|
132
|
+
entity = await service.register({
|
|
133
|
+
hostname: parsed.data.hostname,
|
|
134
|
+
organizationId: parsed.data.organizationId,
|
|
135
|
+
tenantId: auth.tenantId,
|
|
136
|
+
replacesDomainId: parsed.data.replacesDomainId,
|
|
137
|
+
})
|
|
138
|
+
} catch (err: unknown) {
|
|
139
|
+
if (isUniqueViolation(err)) {
|
|
140
|
+
return NextResponse.json(
|
|
141
|
+
{ ok: false, error: 'This domain is not available. Please choose a different hostname.' },
|
|
142
|
+
{ status: 409 },
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
const message = err instanceof Error ? err.message : 'Failed to register domain'
|
|
146
|
+
return NextResponse.json({ ok: false, error: message }, { status: 500 })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
150
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
151
|
+
tenantId: auth.tenantId,
|
|
152
|
+
organizationId: parsed.data.organizationId,
|
|
153
|
+
userId: auth.sub,
|
|
154
|
+
resourceKind: 'customer_accounts.domain_mapping',
|
|
155
|
+
resourceId: entity.id,
|
|
156
|
+
operation: 'create',
|
|
157
|
+
requestMethod: req.method,
|
|
158
|
+
requestHeaders: req.headers,
|
|
159
|
+
metadata: guardResult.metadata ?? null,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return NextResponse.json({ ok: true, domainMapping: serializeRecord(entity) }, { status: 201 })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function DELETE(req: Request) {
|
|
167
|
+
const auth = await getAuthFromRequest(req)
|
|
168
|
+
if (!auth || !auth.tenantId) return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })
|
|
169
|
+
|
|
170
|
+
const container = await createRequestContainer()
|
|
171
|
+
const rbac = container.resolve('rbacService') as RbacService
|
|
172
|
+
const allowed = await rbac.userHasAllFeatures(auth.sub, [FEATURE], {
|
|
173
|
+
tenantId: auth.tenantId,
|
|
174
|
+
organizationId: auth.orgId,
|
|
175
|
+
})
|
|
176
|
+
if (!allowed) return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 })
|
|
177
|
+
|
|
178
|
+
const url = new URL(req.url)
|
|
179
|
+
const id = url.searchParams.get('id')
|
|
180
|
+
if (!id) return NextResponse.json({ ok: false, error: 'id is required' }, { status: 400 })
|
|
181
|
+
|
|
182
|
+
const service = container.resolve('domainMappingService') as DomainMappingService
|
|
183
|
+
const existing = await service.findById(id, { tenantId: auth.tenantId })
|
|
184
|
+
if (!existing) return NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 })
|
|
185
|
+
|
|
186
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
187
|
+
tenantId: auth.tenantId,
|
|
188
|
+
organizationId: existing.organizationId,
|
|
189
|
+
userId: auth.sub,
|
|
190
|
+
resourceKind: 'customer_accounts.domain_mapping',
|
|
191
|
+
resourceId: id,
|
|
192
|
+
operation: 'delete',
|
|
193
|
+
requestMethod: req.method,
|
|
194
|
+
requestHeaders: req.headers,
|
|
195
|
+
})
|
|
196
|
+
if (guardResult && !guardResult.ok) {
|
|
197
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await service.remove(id, { tenantId: auth.tenantId })
|
|
201
|
+
|
|
202
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
203
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
204
|
+
tenantId: auth.tenantId,
|
|
205
|
+
organizationId: existing.organizationId,
|
|
206
|
+
userId: auth.sub,
|
|
207
|
+
resourceKind: 'customer_accounts.domain_mapping',
|
|
208
|
+
resourceId: id,
|
|
209
|
+
operation: 'delete',
|
|
210
|
+
requestMethod: req.method,
|
|
211
|
+
requestHeaders: req.headers,
|
|
212
|
+
metadata: guardResult.metadata ?? null,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return NextResponse.json({ ok: true })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
|
|
220
|
+
const domainMappingSchema = z.object({
|
|
221
|
+
id: z.string().uuid(),
|
|
222
|
+
hostname: z.string(),
|
|
223
|
+
organizationId: z.string().uuid(),
|
|
224
|
+
tenantId: z.string().uuid(),
|
|
225
|
+
provider: z.literal('traefik'),
|
|
226
|
+
status: z.enum(['pending', 'verified', 'active', 'dns_failed', 'tls_failed']),
|
|
227
|
+
verifiedAt: z.string().nullable(),
|
|
228
|
+
lastDnsCheckAt: z.string().nullable(),
|
|
229
|
+
dnsFailureReason: z.string().nullable(),
|
|
230
|
+
tlsFailureReason: z.string().nullable(),
|
|
231
|
+
tlsRetryCount: z.number().int().nonnegative(),
|
|
232
|
+
cnameTarget: z.string().nullable(),
|
|
233
|
+
aRecordTarget: z.string().nullable(),
|
|
234
|
+
createdAt: z.string(),
|
|
235
|
+
updatedAt: z.string().nullable(),
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
export const openApi: OpenApiRouteDoc = {
|
|
239
|
+
tag: 'CustomerAccounts',
|
|
240
|
+
summary: 'Custom portal domain mappings (admin)',
|
|
241
|
+
methods: {
|
|
242
|
+
GET: {
|
|
243
|
+
summary: 'List domain mappings',
|
|
244
|
+
description: 'Returns all custom-domain mappings for the current tenant, optionally filtered by organization.',
|
|
245
|
+
responses: [
|
|
246
|
+
{
|
|
247
|
+
status: 200,
|
|
248
|
+
description: 'OK',
|
|
249
|
+
schema: z.object({
|
|
250
|
+
ok: z.literal(true),
|
|
251
|
+
domainMappings: z.array(domainMappingSchema),
|
|
252
|
+
config: z.object({
|
|
253
|
+
cnameTarget: z.string().nullable(),
|
|
254
|
+
aRecordTarget: z.string().nullable(),
|
|
255
|
+
}),
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
errors: [
|
|
260
|
+
{ status: 401, description: 'Unauthorized', schema: errorSchema },
|
|
261
|
+
{ status: 403, description: 'Forbidden', schema: errorSchema },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
POST: {
|
|
265
|
+
summary: 'Register a custom domain',
|
|
266
|
+
description: 'Registers a new custom domain mapping for an organization. Verifies via DNS asynchronously.',
|
|
267
|
+
requestBody: {
|
|
268
|
+
contentType: 'application/json',
|
|
269
|
+
schema: z.object({
|
|
270
|
+
hostname: z.string(),
|
|
271
|
+
organizationId: z.string().uuid(),
|
|
272
|
+
replacesDomainId: z.string().uuid().optional(),
|
|
273
|
+
}),
|
|
274
|
+
},
|
|
275
|
+
responses: [
|
|
276
|
+
{
|
|
277
|
+
status: 201,
|
|
278
|
+
description: 'Created',
|
|
279
|
+
schema: z.object({ ok: z.literal(true), domainMapping: domainMappingSchema }),
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
errors: [
|
|
283
|
+
{ status: 400, description: 'Validation error', schema: errorSchema },
|
|
284
|
+
{ status: 401, description: 'Unauthorized', schema: errorSchema },
|
|
285
|
+
{ status: 403, description: 'Forbidden', schema: errorSchema },
|
|
286
|
+
{ status: 409, description: 'Conflict', schema: errorSchema },
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
DELETE: {
|
|
290
|
+
summary: 'Remove a custom domain',
|
|
291
|
+
description: 'Removes the domain mapping identified by ?id=. Cache and Traefik routing drain within TTL.',
|
|
292
|
+
responses: [{ status: 200, description: 'OK', schema: z.object({ ok: z.literal(true) }) }],
|
|
293
|
+
errors: [
|
|
294
|
+
{ status: 400, description: 'Bad request', schema: errorSchema },
|
|
295
|
+
{ status: 401, description: 'Unauthorized', schema: errorSchema },
|
|
296
|
+
{ status: 403, description: 'Forbidden', schema: errorSchema },
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Re-export resolve type so tests can import without indirect lookup
|
|
303
|
+
export type { ResolveResult }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
|
|
6
|
+
import { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'
|
|
7
|
+
import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
|
|
8
|
+
|
|
9
|
+
const ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'
|
|
10
|
+
|
|
11
|
+
export const metadata = {
|
|
12
|
+
GET: { requireAuth: false },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function withOriginHeader(response: NextResponse): NextResponse {
|
|
16
|
+
response.headers.set(ORIGIN_HEADER_NAME, '1')
|
|
17
|
+
return response
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unauthorized(message: string): NextResponse {
|
|
21
|
+
return withOriginHeader(NextResponse.json({ ok: false, error: message }, { status: 403 }))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function notFound(): NextResponse {
|
|
25
|
+
return withOriginHeader(NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 }))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ok(): NextResponse {
|
|
29
|
+
return withOriginHeader(NextResponse.json({ ok: true }))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function GET(req: Request) {
|
|
33
|
+
const expected = process.env.DOMAIN_CHECK_SECRET
|
|
34
|
+
if (!expected) {
|
|
35
|
+
// Fail closed when the gating secret is not configured on the server.
|
|
36
|
+
return withOriginHeader(
|
|
37
|
+
NextResponse.json(
|
|
38
|
+
{ ok: false, error: 'DOMAIN_CHECK_SECRET is not configured on the server' },
|
|
39
|
+
{ status: 503 },
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
const supplied = req.headers.get('x-domain-check-secret')
|
|
44
|
+
if (!secretEqual(supplied, expected)) return unauthorized('Forbidden')
|
|
45
|
+
|
|
46
|
+
const url = new URL(req.url)
|
|
47
|
+
// Traefik's ForwardAuth middleware cannot template the auth URL with the
|
|
48
|
+
// original Host, so it forwards the original request via X-Forwarded-Host.
|
|
49
|
+
// Manual / direct callers can still pass `?host=` explicitly.
|
|
50
|
+
const rawHost =
|
|
51
|
+
url.searchParams.get('host') ?? req.headers.get('x-forwarded-host')
|
|
52
|
+
if (!rawHost) return notFound()
|
|
53
|
+
const hostname = tryNormalizeHostname(rawHost)
|
|
54
|
+
if (!hostname) return notFound()
|
|
55
|
+
|
|
56
|
+
const container = await createRequestContainer()
|
|
57
|
+
const service = container.resolve('domainMappingService') as DomainMappingService
|
|
58
|
+
const result = await service.isAllowedForTls(hostname)
|
|
59
|
+
if (!result) return notFound()
|
|
60
|
+
return ok()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
|
|
64
|
+
|
|
65
|
+
export const openApi: OpenApiRouteDoc = {
|
|
66
|
+
tag: 'CustomerAccounts',
|
|
67
|
+
summary: 'Traefik domain-check gate',
|
|
68
|
+
methods: {
|
|
69
|
+
GET: {
|
|
70
|
+
summary: 'Verify a hostname is allowed for TLS provisioning',
|
|
71
|
+
description:
|
|
72
|
+
'Called by Traefik before issuing a Let\'s Encrypt certificate. Requires the X-Domain-Check-Secret header to match DOMAIN_CHECK_SECRET.',
|
|
73
|
+
responses: [
|
|
74
|
+
{ status: 200, description: 'Hostname allowed', schema: z.object({ ok: z.literal(true) }) },
|
|
75
|
+
],
|
|
76
|
+
errors: [
|
|
77
|
+
{ status: 403, description: 'Forbidden', schema: errorSchema },
|
|
78
|
+
{ status: 404, description: 'Hostname not registered or not yet verified', schema: errorSchema },
|
|
79
|
+
{ status: 503, description: 'Server misconfiguration', schema: errorSchema },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'
|
|
6
|
+
import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
|
|
7
|
+
import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
8
|
+
import {
|
|
9
|
+
checkAuthRateLimit,
|
|
10
|
+
domainResolveAllIpRateLimitConfig,
|
|
11
|
+
} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'
|
|
12
|
+
|
|
13
|
+
const ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'
|
|
14
|
+
const LOG_PREFIX = '[domain-resolve/all]'
|
|
15
|
+
const ENDPOINT_PATH = '/api/customer_accounts/domain-resolve/all'
|
|
16
|
+
|
|
17
|
+
export const metadata = {
|
|
18
|
+
GET: { requireAuth: false },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function withOriginHeader(response: NextResponse): NextResponse {
|
|
22
|
+
response.headers.set(ORIGIN_HEADER_NAME, '1')
|
|
23
|
+
return response
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readCallerIp(req: Request): string {
|
|
27
|
+
const forwardedFor = req.headers.get('x-forwarded-for')
|
|
28
|
+
if (forwardedFor) {
|
|
29
|
+
const firstHop = forwardedFor.split(',')[0]?.trim()
|
|
30
|
+
if (firstHop) return firstHop
|
|
31
|
+
}
|
|
32
|
+
const realIp = req.headers.get('x-real-ip')
|
|
33
|
+
if (realIp && realIp.trim().length > 0) return realIp.trim()
|
|
34
|
+
return 'unknown'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function GET(req: Request) {
|
|
38
|
+
const callerIp = readCallerIp(req)
|
|
39
|
+
const expected = process.env.DOMAIN_RESOLVE_SECRET
|
|
40
|
+
if (!expected) {
|
|
41
|
+
// Fail closed when the gating secret is not configured on the server.
|
|
42
|
+
console.warn(`${LOG_PREFIX} rejected request — secret not configured`, {
|
|
43
|
+
endpoint: ENDPOINT_PATH,
|
|
44
|
+
ip: callerIp,
|
|
45
|
+
outcome: 'misconfigured',
|
|
46
|
+
})
|
|
47
|
+
return withOriginHeader(
|
|
48
|
+
NextResponse.json(
|
|
49
|
+
{ ok: false, error: 'DOMAIN_RESOLVE_SECRET is not configured on the server' },
|
|
50
|
+
{ status: 503 },
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Rate-limit by caller IP before the secret comparison to make leak-driven
|
|
56
|
+
// enumeration noisy and bounded. Fails closed (returns the helper's 429
|
|
57
|
+
// response) when the limit is exceeded.
|
|
58
|
+
const { error: rateLimitError } = await checkAuthRateLimit({
|
|
59
|
+
req,
|
|
60
|
+
ipConfig: domainResolveAllIpRateLimitConfig,
|
|
61
|
+
})
|
|
62
|
+
if (rateLimitError) {
|
|
63
|
+
console.warn(`${LOG_PREFIX} rate-limited`, {
|
|
64
|
+
endpoint: ENDPOINT_PATH,
|
|
65
|
+
ip: callerIp,
|
|
66
|
+
outcome: 'rate_limited',
|
|
67
|
+
})
|
|
68
|
+
return withOriginHeader(rateLimitError)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const supplied = req.headers.get('x-domain-resolve-secret')
|
|
72
|
+
if (!secretEqual(supplied, expected)) {
|
|
73
|
+
console.warn(`${LOG_PREFIX} rejected request — invalid secret`, {
|
|
74
|
+
endpoint: ENDPOINT_PATH,
|
|
75
|
+
ip: callerIp,
|
|
76
|
+
outcome: 'forbidden',
|
|
77
|
+
})
|
|
78
|
+
return withOriginHeader(NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 }))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const container = await createRequestContainer()
|
|
82
|
+
const service = container.resolve('domainMappingService') as DomainMappingService
|
|
83
|
+
const records = await service.resolveAll()
|
|
84
|
+
console.info(`${LOG_PREFIX} authorized`, {
|
|
85
|
+
endpoint: ENDPOINT_PATH,
|
|
86
|
+
ip: callerIp,
|
|
87
|
+
outcome: 'ok',
|
|
88
|
+
domains: records.length,
|
|
89
|
+
})
|
|
90
|
+
return withOriginHeader(
|
|
91
|
+
NextResponse.json({
|
|
92
|
+
ok: true,
|
|
93
|
+
domains: records.map((r) => ({
|
|
94
|
+
hostname: r.hostname,
|
|
95
|
+
tenantId: r.tenantId,
|
|
96
|
+
organizationId: r.organizationId,
|
|
97
|
+
orgSlug: r.orgSlug,
|
|
98
|
+
status: r.status,
|
|
99
|
+
})),
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
|
|
105
|
+
|
|
106
|
+
export const openApi: OpenApiRouteDoc = {
|
|
107
|
+
tag: 'CustomerAccounts',
|
|
108
|
+
summary: 'Batch resolve all active custom domains',
|
|
109
|
+
methods: {
|
|
110
|
+
GET: {
|
|
111
|
+
summary: 'Batch warm-up for the Node middleware',
|
|
112
|
+
description:
|
|
113
|
+
'Returns every active domain mapping in a single payload so the middleware can populate its cache on process start. Requires X-Domain-Resolve-Secret header.',
|
|
114
|
+
responses: [
|
|
115
|
+
{
|
|
116
|
+
status: 200,
|
|
117
|
+
description: 'OK',
|
|
118
|
+
schema: z.object({
|
|
119
|
+
ok: z.literal(true),
|
|
120
|
+
domains: z.array(
|
|
121
|
+
z.object({
|
|
122
|
+
hostname: z.string(),
|
|
123
|
+
tenantId: z.string().uuid(),
|
|
124
|
+
organizationId: z.string().uuid(),
|
|
125
|
+
orgSlug: z.string().nullable(),
|
|
126
|
+
status: z.literal('active'),
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
errors: [
|
|
133
|
+
{ status: 403, description: 'Forbidden', schema: errorSchema },
|
|
134
|
+
{ status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },
|
|
135
|
+
{ status: 503, description: 'Server misconfiguration', schema: errorSchema },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
|
|
6
|
+
import { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'
|
|
7
|
+
import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
|
|
8
|
+
|
|
9
|
+
const ORIGIN_HEADER_NAME = process.env.CUSTOMER_DOMAIN_ORIGIN_HEADER ?? 'X-Open-Mercato-Origin'
|
|
10
|
+
|
|
11
|
+
export const metadata = {
|
|
12
|
+
GET: { requireAuth: false },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function withOriginHeader(response: NextResponse): NextResponse {
|
|
16
|
+
response.headers.set(ORIGIN_HEADER_NAME, '1')
|
|
17
|
+
return response
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function GET(req: Request) {
|
|
21
|
+
const expected = process.env.DOMAIN_RESOLVE_SECRET
|
|
22
|
+
if (!expected) {
|
|
23
|
+
// Fail closed when the gating secret is not configured on the server.
|
|
24
|
+
return withOriginHeader(
|
|
25
|
+
NextResponse.json(
|
|
26
|
+
{ ok: false, error: 'DOMAIN_RESOLVE_SECRET is not configured on the server' },
|
|
27
|
+
{ status: 503 },
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
const supplied = req.headers.get('x-domain-resolve-secret')
|
|
32
|
+
if (!secretEqual(supplied, expected)) {
|
|
33
|
+
return withOriginHeader(NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 }))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const url = new URL(req.url)
|
|
37
|
+
const rawHost = url.searchParams.get('host')
|
|
38
|
+
if (!rawHost) return withOriginHeader(NextResponse.json({ ok: false, error: 'host param required' }, { status: 400 }))
|
|
39
|
+
const hostname = tryNormalizeHostname(rawHost)
|
|
40
|
+
if (!hostname) return withOriginHeader(NextResponse.json({ ok: false, error: 'invalid host' }, { status: 400 }))
|
|
41
|
+
|
|
42
|
+
const container = await createRequestContainer()
|
|
43
|
+
const service = container.resolve('domainMappingService') as DomainMappingService
|
|
44
|
+
const result = await service.resolveByHostname(hostname)
|
|
45
|
+
if (!result || result.status !== 'active') {
|
|
46
|
+
return withOriginHeader(NextResponse.json({ ok: false, error: 'Not found' }, { status: 404 }))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return withOriginHeader(
|
|
50
|
+
NextResponse.json({
|
|
51
|
+
ok: true,
|
|
52
|
+
tenantId: result.tenantId,
|
|
53
|
+
organizationId: result.organizationId,
|
|
54
|
+
orgSlug: result.orgSlug,
|
|
55
|
+
status: result.status,
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
|
|
61
|
+
|
|
62
|
+
export const openApi: OpenApiRouteDoc = {
|
|
63
|
+
tag: 'CustomerAccounts',
|
|
64
|
+
summary: 'Resolve a custom domain to its tenant and organization',
|
|
65
|
+
methods: {
|
|
66
|
+
GET: {
|
|
67
|
+
summary: 'Single-host resolve for the Node middleware',
|
|
68
|
+
description:
|
|
69
|
+
'Internal endpoint used by the portal middleware to populate its in-memory cache. Requires X-Domain-Resolve-Secret header.',
|
|
70
|
+
responses: [
|
|
71
|
+
{
|
|
72
|
+
status: 200,
|
|
73
|
+
description: 'OK',
|
|
74
|
+
schema: z.object({
|
|
75
|
+
ok: z.literal(true),
|
|
76
|
+
tenantId: z.string().uuid(),
|
|
77
|
+
organizationId: z.string().uuid(),
|
|
78
|
+
orgSlug: z.string().nullable(),
|
|
79
|
+
status: z.literal('active'),
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
errors: [
|
|
84
|
+
{ status: 400, description: 'Bad request', schema: errorSchema },
|
|
85
|
+
{ status: 403, description: 'Forbidden', schema: errorSchema },
|
|
86
|
+
{ status: 404, description: 'Not found', schema: errorSchema },
|
|
87
|
+
{ status: 503, description: 'Server misconfiguration', schema: errorSchema },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
customerLoginRateLimitConfig,
|
|
16
16
|
customerLoginIpRateLimitConfig,
|
|
17
17
|
} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'
|
|
18
|
+
import {
|
|
19
|
+
resolveTenantContext,
|
|
20
|
+
TenantResolutionError,
|
|
21
|
+
} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'
|
|
18
22
|
|
|
19
23
|
export const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }
|
|
20
24
|
|
|
@@ -31,9 +35,16 @@ export async function POST(req: Request) {
|
|
|
31
35
|
return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
const { email, password
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
const { email, password } = parsed.data
|
|
39
|
+
let tenantId: string
|
|
40
|
+
try {
|
|
41
|
+
const context = await resolveTenantContext(req, parsed.data.tenantId)
|
|
42
|
+
tenantId = context.tenantId
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err instanceof TenantResolutionError) {
|
|
45
|
+
return NextResponse.json({ ok: false, error: err.message }, { status: err.status })
|
|
46
|
+
}
|
|
47
|
+
throw err
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({
|
|
@@ -2,6 +2,10 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
4
|
import { magicLinkRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
5
|
+
import {
|
|
6
|
+
resolveTenantContext,
|
|
7
|
+
TenantResolutionError,
|
|
8
|
+
} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'
|
|
5
9
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
10
|
import { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'
|
|
7
11
|
import { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'
|
|
@@ -37,9 +41,16 @@ export async function POST(req: Request) {
|
|
|
37
41
|
return NextResponse.json({ ok: true })
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
const { email
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
const { email } = parsed.data
|
|
45
|
+
let tenantId: string
|
|
46
|
+
try {
|
|
47
|
+
const context = await resolveTenantContext(req, parsed.data.tenantId)
|
|
48
|
+
tenantId = context.tenantId
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof TenantResolutionError) {
|
|
51
|
+
return NextResponse.json({ ok: true })
|
|
52
|
+
}
|
|
53
|
+
throw err
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
const container = await createRequestContainer()
|