@open-mercato/core 0.6.1-develop.3260.1.dc719cac84 → 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
-
import {
|
|
3
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
+
import { CustomerUser, DomainMapping, type DomainStatus } from './entities'
|
|
4
5
|
|
|
5
6
|
type EntityRecord = Record<string, unknown> & { id: string }
|
|
6
7
|
type EnricherScope = EnricherContext & { em: EntityManager }
|
|
@@ -42,11 +43,17 @@ const personAccountStatusEnricher: ResponseEnricher<EntityRecord, PersonAccountE
|
|
|
42
43
|
const em = context.em.fork()
|
|
43
44
|
const recordIds = records.map((r) => r.id)
|
|
44
45
|
|
|
45
|
-
const users = await
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const users = await findWithDecryption(
|
|
47
|
+
em,
|
|
48
|
+
CustomerUser,
|
|
49
|
+
{
|
|
50
|
+
personEntityId: { $in: recordIds },
|
|
51
|
+
tenantId: context.tenantId,
|
|
52
|
+
deletedAt: null,
|
|
53
|
+
},
|
|
54
|
+
undefined,
|
|
55
|
+
{ tenantId: context.tenantId, organizationId: context.organizationId },
|
|
56
|
+
)
|
|
50
57
|
|
|
51
58
|
const userByPersonId = new Map<string, CustomerUser>()
|
|
52
59
|
for (const user of users) {
|
|
@@ -92,11 +99,17 @@ const companyUserCountEnricher: ResponseEnricher<EntityRecord, CompanyUserCountE
|
|
|
92
99
|
const em = context.em.fork()
|
|
93
100
|
const recordIds = records.map((r) => r.id)
|
|
94
101
|
|
|
95
|
-
const users = await
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
const users = await findWithDecryption(
|
|
103
|
+
em,
|
|
104
|
+
CustomerUser,
|
|
105
|
+
{
|
|
106
|
+
customerEntityId: { $in: recordIds },
|
|
107
|
+
tenantId: context.tenantId,
|
|
108
|
+
deletedAt: null,
|
|
109
|
+
},
|
|
110
|
+
undefined,
|
|
111
|
+
{ tenantId: context.tenantId, organizationId: context.organizationId },
|
|
112
|
+
)
|
|
100
113
|
|
|
101
114
|
const countByCompanyId = new Map<string, number>()
|
|
102
115
|
for (const user of users) {
|
|
@@ -114,4 +127,74 @@ const companyUserCountEnricher: ResponseEnricher<EntityRecord, CompanyUserCountE
|
|
|
114
127
|
},
|
|
115
128
|
}
|
|
116
129
|
|
|
117
|
-
|
|
130
|
+
type OrgCustomDomainEnrichment = {
|
|
131
|
+
_customDomain: { hostname: string; status: DomainStatus } | null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ORG_DOMAIN_FALLBACK: OrgCustomDomainEnrichment = { _customDomain: null }
|
|
135
|
+
|
|
136
|
+
const ACTIVE_DOMAIN_PRIORITY: Record<DomainStatus, number> = {
|
|
137
|
+
active: 0,
|
|
138
|
+
verified: 1,
|
|
139
|
+
pending: 2,
|
|
140
|
+
tls_failed: 3,
|
|
141
|
+
dns_failed: 4,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pickPrimaryDomain(records: DomainMapping[]): DomainMapping | null {
|
|
145
|
+
if (records.length === 0) return null
|
|
146
|
+
return records
|
|
147
|
+
.slice()
|
|
148
|
+
.sort(
|
|
149
|
+
(a, b) =>
|
|
150
|
+
(ACTIVE_DOMAIN_PRIORITY[a.status] ?? 99) - (ACTIVE_DOMAIN_PRIORITY[b.status] ?? 99) ||
|
|
151
|
+
(b.createdAt?.getTime?.() ?? 0) - (a.createdAt?.getTime?.() ?? 0),
|
|
152
|
+
)[0]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const organizationCustomDomainEnricher: ResponseEnricher<EntityRecord, OrgCustomDomainEnrichment> = {
|
|
156
|
+
id: 'customer_accounts.domain-status:directory:organization',
|
|
157
|
+
targetEntity: 'directory:organization',
|
|
158
|
+
features: ['customer_accounts.domain.manage'],
|
|
159
|
+
priority: 10,
|
|
160
|
+
timeout: 1_000,
|
|
161
|
+
critical: false,
|
|
162
|
+
fallback: ORG_DOMAIN_FALLBACK,
|
|
163
|
+
|
|
164
|
+
async enrichOne(record, context: EnricherScope) {
|
|
165
|
+
return (await this.enrichMany!([record], context))[0]
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async enrichMany(records, context: EnricherScope) {
|
|
169
|
+
if (records.length === 0) return records as (EntityRecord & OrgCustomDomainEnrichment)[]
|
|
170
|
+
|
|
171
|
+
const em = context.em.fork()
|
|
172
|
+
const recordIds = records.map((r) => r.id)
|
|
173
|
+
const domains = await em.find(DomainMapping, {
|
|
174
|
+
organizationId: { $in: recordIds },
|
|
175
|
+
tenantId: context.tenantId,
|
|
176
|
+
} as never)
|
|
177
|
+
|
|
178
|
+
const byOrg = new Map<string, DomainMapping[]>()
|
|
179
|
+
for (const d of domains) {
|
|
180
|
+
const list = byOrg.get(d.organizationId) ?? []
|
|
181
|
+
list.push(d)
|
|
182
|
+
byOrg.set(d.organizationId, list)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return records.map((record) => {
|
|
186
|
+
const matches = byOrg.get(record.id) ?? []
|
|
187
|
+
const primary = pickPrimaryDomain(matches)
|
|
188
|
+
return {
|
|
189
|
+
...record,
|
|
190
|
+
_customDomain: primary ? { hostname: primary.hostname, status: primary.status } : null,
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const enrichers: ResponseEnricher[] = [
|
|
197
|
+
personAccountStatusEnricher,
|
|
198
|
+
companyUserCountEnricher,
|
|
199
|
+
organizationCustomDomainEnricher,
|
|
200
|
+
]
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import { Entity, Index, ManyToOne, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
|
|
1
|
+
import { Check, Entity, Enum, Index, ManyToOne, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
|
|
2
|
+
|
|
3
|
+
export type DomainProvider = 'traefik'
|
|
4
|
+
export type DomainStatus = 'pending' | 'verified' | 'active' | 'dns_failed' | 'tls_failed'
|
|
5
|
+
|
|
6
|
+
export const DOMAIN_PROVIDERS: readonly DomainProvider[] = ['traefik'] as const
|
|
7
|
+
export const DOMAIN_STATUSES: readonly DomainStatus[] = [
|
|
8
|
+
'pending',
|
|
9
|
+
'verified',
|
|
10
|
+
'active',
|
|
11
|
+
'dns_failed',
|
|
12
|
+
'tls_failed',
|
|
13
|
+
] as const
|
|
2
14
|
|
|
3
15
|
@Entity({ tableName: 'customer_users' })
|
|
4
16
|
@Unique({ properties: ['tenantId', 'emailHash'], name: 'customer_users_tenant_email_hash_uniq' })
|
|
@@ -303,3 +315,70 @@ export class CustomerUserInvitation {
|
|
|
303
315
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
304
316
|
createdAt: Date = new Date()
|
|
305
317
|
}
|
|
318
|
+
|
|
319
|
+
@Entity({ tableName: 'domain_mappings' })
|
|
320
|
+
@Unique({ properties: ['hostname'], name: 'domain_mappings_hostname_unique' })
|
|
321
|
+
@Index({ properties: ['organizationId'], name: 'domain_mappings_organization_id_idx' })
|
|
322
|
+
@Index({ properties: ['tenantId'], name: 'domain_mappings_tenant_id_idx' })
|
|
323
|
+
@Unique({
|
|
324
|
+
name: 'domain_mappings_replaces_domain_id_unique',
|
|
325
|
+
expression:
|
|
326
|
+
`create unique index "domain_mappings_replaces_domain_id_unique" on "domain_mappings" ("replaces_domain_id") where "replaces_domain_id" is not null`,
|
|
327
|
+
})
|
|
328
|
+
@Index({
|
|
329
|
+
name: 'domain_mappings_pending_verification_idx',
|
|
330
|
+
expression:
|
|
331
|
+
`create index "domain_mappings_pending_verification_idx" on "domain_mappings" ("status", "last_dns_check_at") where "status" in ('pending', 'dns_failed')`,
|
|
332
|
+
})
|
|
333
|
+
@Index({
|
|
334
|
+
name: 'domain_mappings_pending_tls_idx',
|
|
335
|
+
expression:
|
|
336
|
+
`create index "domain_mappings_pending_tls_idx" on "domain_mappings" ("status", "updated_at") where "status" in ('verified', 'tls_failed')`,
|
|
337
|
+
})
|
|
338
|
+
@Check({
|
|
339
|
+
name: 'domain_mappings_hostname_normalized_chk',
|
|
340
|
+
expression: `"hostname" = lower("hostname") and "hostname" not like '%.'`,
|
|
341
|
+
})
|
|
342
|
+
export class DomainMapping {
|
|
343
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
344
|
+
id!: string
|
|
345
|
+
|
|
346
|
+
@Property({ type: 'text' })
|
|
347
|
+
hostname!: string
|
|
348
|
+
|
|
349
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
350
|
+
tenantId!: string
|
|
351
|
+
|
|
352
|
+
@Property({ name: 'organization_id', type: 'uuid' })
|
|
353
|
+
organizationId!: string
|
|
354
|
+
|
|
355
|
+
@ManyToOne(() => DomainMapping, { fieldName: 'replaces_domain_id', nullable: true })
|
|
356
|
+
replacesDomain?: DomainMapping | null
|
|
357
|
+
|
|
358
|
+
@Enum({ items: () => DOMAIN_PROVIDERS as unknown as string[], type: 'text', name: 'provider' })
|
|
359
|
+
provider: DomainProvider = 'traefik'
|
|
360
|
+
|
|
361
|
+
@Enum({ items: () => DOMAIN_STATUSES as unknown as string[], type: 'text', name: 'status' })
|
|
362
|
+
status: DomainStatus = 'pending'
|
|
363
|
+
|
|
364
|
+
@Property({ name: 'verified_at', type: Date, nullable: true })
|
|
365
|
+
verifiedAt?: Date | null
|
|
366
|
+
|
|
367
|
+
@Property({ name: 'last_dns_check_at', type: Date, nullable: true })
|
|
368
|
+
lastDnsCheckAt?: Date | null
|
|
369
|
+
|
|
370
|
+
@Property({ name: 'dns_failure_reason', type: 'text', nullable: true, length: 500 })
|
|
371
|
+
dnsFailureReason?: string | null
|
|
372
|
+
|
|
373
|
+
@Property({ name: 'tls_failure_reason', type: 'text', nullable: true, length: 500 })
|
|
374
|
+
tlsFailureReason?: string | null
|
|
375
|
+
|
|
376
|
+
@Property({ name: 'tls_retry_count', type: 'int', default: 0 })
|
|
377
|
+
tlsRetryCount: number = 0
|
|
378
|
+
|
|
379
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
380
|
+
createdAt: Date = new Date()
|
|
381
|
+
|
|
382
|
+
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })
|
|
383
|
+
updatedAt?: Date | null
|
|
384
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { MutationGuard } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
|
|
2
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
|
|
4
|
+
import type { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
|
|
5
|
+
|
|
6
|
+
const DOMAIN_MAPPING_ENTITY = 'customer_accounts.domain_mapping'
|
|
7
|
+
const MAX_DOMAINS_PER_ORG = 2
|
|
8
|
+
|
|
9
|
+
function readHostnameField(payload: Record<string, unknown> | null | undefined): string | null {
|
|
10
|
+
if (!payload || typeof payload !== 'object') return null
|
|
11
|
+
const raw = (payload as Record<string, unknown>).hostname
|
|
12
|
+
return typeof raw === 'string' ? raw : null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readOrgIdField(payload: Record<string, unknown> | null | undefined): string | null {
|
|
16
|
+
if (!payload || typeof payload !== 'object') return null
|
|
17
|
+
const raw = (payload as Record<string, unknown>).organizationId ?? (payload as Record<string, unknown>).organization_id
|
|
18
|
+
return typeof raw === 'string' ? raw : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const hostnameFormatGuard: MutationGuard = {
|
|
22
|
+
id: 'customer_accounts.domain_mapping.hostname-format',
|
|
23
|
+
targetEntity: DOMAIN_MAPPING_ENTITY,
|
|
24
|
+
operations: ['create'],
|
|
25
|
+
priority: 10,
|
|
26
|
+
|
|
27
|
+
async validate(input) {
|
|
28
|
+
const raw = readHostnameField(input.mutationPayload)
|
|
29
|
+
if (!raw) {
|
|
30
|
+
return { ok: false, status: 422, message: 'Hostname is required' }
|
|
31
|
+
}
|
|
32
|
+
const normalized = tryNormalizeHostname(raw)
|
|
33
|
+
if (!normalized) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
status: 422,
|
|
37
|
+
message: 'Hostname is not a valid DNS name (must be at least two labels, ASCII or IDN)',
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
modifiedPayload: { hostname: normalized },
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hostnameUniqueGuard: MutationGuard = {
|
|
48
|
+
id: 'customer_accounts.domain_mapping.hostname-unique',
|
|
49
|
+
targetEntity: DOMAIN_MAPPING_ENTITY,
|
|
50
|
+
operations: ['create'],
|
|
51
|
+
priority: 20,
|
|
52
|
+
|
|
53
|
+
async validate(input) {
|
|
54
|
+
const raw = readHostnameField(input.mutationPayload)
|
|
55
|
+
if (!raw) return { ok: true }
|
|
56
|
+
const hostname = tryNormalizeHostname(raw)
|
|
57
|
+
if (!hostname) return { ok: true }
|
|
58
|
+
|
|
59
|
+
const container = await createRequestContainer()
|
|
60
|
+
let service: DomainMappingService | null = null
|
|
61
|
+
try {
|
|
62
|
+
service = container.resolve('domainMappingService') as DomainMappingService
|
|
63
|
+
} catch {
|
|
64
|
+
return { ok: true }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const existing = await service.resolveByHostname(hostname)
|
|
68
|
+
if (!existing) return { ok: true }
|
|
69
|
+
|
|
70
|
+
// Intentionally tenant-agnostic message: do not disclose whether the
|
|
71
|
+
// collision is intra-tenant or cross-tenant. Distinguishing them would
|
|
72
|
+
// let any caller probe deployment-wide hostname registration.
|
|
73
|
+
const sameOrg = existing.organizationId === readOrgIdField(input.mutationPayload)
|
|
74
|
+
if (existing.tenantId !== input.tenantId || !sameOrg) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
status: 409,
|
|
78
|
+
message: 'This domain is not available. Please choose a different hostname.',
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { ok: true }
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const orgLimitGuard: MutationGuard = {
|
|
86
|
+
id: 'customer_accounts.domain_mapping.org-limit',
|
|
87
|
+
targetEntity: DOMAIN_MAPPING_ENTITY,
|
|
88
|
+
operations: ['create'],
|
|
89
|
+
priority: 30,
|
|
90
|
+
|
|
91
|
+
async validate(input) {
|
|
92
|
+
const orgId = readOrgIdField(input.mutationPayload) ?? input.organizationId
|
|
93
|
+
if (!orgId) {
|
|
94
|
+
return { ok: false, status: 422, message: 'Organization is required' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const container = await createRequestContainer()
|
|
98
|
+
let service: DomainMappingService | null = null
|
|
99
|
+
try {
|
|
100
|
+
service = container.resolve('domainMappingService') as DomainMappingService
|
|
101
|
+
} catch {
|
|
102
|
+
return { ok: true }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const existing = await service.findByOrganization(orgId, { tenantId: input.tenantId })
|
|
106
|
+
if (existing.length >= MAX_DOMAINS_PER_ORG) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
status: 409,
|
|
110
|
+
message: `Each organization can have at most ${MAX_DOMAINS_PER_ORG} custom domains (one active and one pending replacement). Remove an existing domain or finish the swap first.`,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { ok: true }
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Mirrors the in-progress-lock pattern used by other modules' update/delete
|
|
118
|
+
// guards: ensure the target record exists and belongs to the caller's tenant
|
|
119
|
+
// before any destructive or lifecycle-changing mutation runs. Verification
|
|
120
|
+
// and health-check endpoints invoke this via operation: 'update' since they
|
|
121
|
+
// transition the record's lifecycle status.
|
|
122
|
+
const recordScopeGuard: MutationGuard = {
|
|
123
|
+
id: 'customer_accounts.domain_mapping.record-scope',
|
|
124
|
+
targetEntity: DOMAIN_MAPPING_ENTITY,
|
|
125
|
+
operations: ['update', 'delete'],
|
|
126
|
+
priority: 10,
|
|
127
|
+
|
|
128
|
+
async validate(input) {
|
|
129
|
+
const resourceId = input.resourceId
|
|
130
|
+
if (!resourceId) {
|
|
131
|
+
return { ok: false, status: 422, message: 'Domain mapping id is required' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const container = await createRequestContainer()
|
|
135
|
+
let service: DomainMappingService | null = null
|
|
136
|
+
try {
|
|
137
|
+
service = container.resolve('domainMappingService') as DomainMappingService
|
|
138
|
+
} catch {
|
|
139
|
+
return { ok: true }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const record = await service.findById(resourceId, { tenantId: input.tenantId })
|
|
143
|
+
if (!record) {
|
|
144
|
+
return { ok: false, status: 404, message: 'Domain mapping not found' }
|
|
145
|
+
}
|
|
146
|
+
return { ok: true }
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const guards: MutationGuard[] = [
|
|
151
|
+
hostnameFormatGuard,
|
|
152
|
+
hostnameUniqueGuard,
|
|
153
|
+
orgLimitGuard,
|
|
154
|
+
recordScopeGuard,
|
|
155
|
+
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { normalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'
|
|
2
3
|
|
|
3
4
|
const emailField = z.string().email().max(255)
|
|
4
5
|
const passwordField = z.string().min(8).max(128)
|
|
@@ -126,3 +127,28 @@ export type UpdateRoleAclInput = z.infer<typeof updateRoleAclSchema>
|
|
|
126
127
|
export type InviteUserInput = z.infer<typeof inviteUserSchema>
|
|
127
128
|
export type AssignRolesInput = z.infer<typeof assignRolesSchema>
|
|
128
129
|
export type AdminUpdateUserInput = z.infer<typeof adminUpdateUserSchema>
|
|
130
|
+
|
|
131
|
+
// Custom domain mapping
|
|
132
|
+
export const hostnameSchema = z
|
|
133
|
+
.string()
|
|
134
|
+
.min(1, 'Hostname is required')
|
|
135
|
+
.max(512, 'Hostname is too long')
|
|
136
|
+
.transform((value, ctx) => {
|
|
137
|
+
try {
|
|
138
|
+
return normalizeHostname(value)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
ctx.addIssue({
|
|
141
|
+
code: z.ZodIssueCode.custom,
|
|
142
|
+
message: err instanceof Error ? err.message : 'Invalid hostname',
|
|
143
|
+
})
|
|
144
|
+
return z.NEVER
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
export const registerDomainSchema = z.object({
|
|
149
|
+
hostname: hostnameSchema,
|
|
150
|
+
organizationId: z.string().uuid(),
|
|
151
|
+
replacesDomainId: z.string().uuid().optional(),
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
export type RegisterDomainInput = z.infer<typeof registerDomainSchema>
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import { asClass } from 'awilix'
|
|
1
|
+
import { asClass, asFunction } from 'awilix'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
3
|
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
4
|
import { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'
|
|
4
5
|
import { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'
|
|
5
6
|
import { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'
|
|
6
7
|
import { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'
|
|
7
8
|
import { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'
|
|
9
|
+
import { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'
|
|
10
|
+
|
|
11
|
+
type DomainCacheService = {
|
|
12
|
+
get: (key: string, options?: unknown) => Promise<unknown>
|
|
13
|
+
set: (key: string, value: unknown, options?: { ttl?: number; tags?: string[] }) => Promise<void>
|
|
14
|
+
deleteByTags: (tags: string[]) => Promise<number>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isDomainCacheService(candidate: unknown): candidate is DomainCacheService {
|
|
18
|
+
if (!candidate || typeof candidate !== 'object') return false
|
|
19
|
+
const c = candidate as Record<string, unknown>
|
|
20
|
+
return typeof c.get === 'function' && typeof c.set === 'function' && typeof c.deleteByTags === 'function'
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
export function register(container: AppContainer) {
|
|
10
24
|
container.register({ customerUserService: asClass(CustomerUserService).scoped() })
|
|
@@ -12,4 +26,24 @@ export function register(container: AppContainer) {
|
|
|
12
26
|
container.register({ customerTokenService: asClass(CustomerTokenService).scoped() })
|
|
13
27
|
container.register({ customerRbacService: asClass(CustomerRbacService).scoped() })
|
|
14
28
|
container.register({ customerInvitationService: asClass(CustomerInvitationService).scoped() })
|
|
29
|
+
container.register({
|
|
30
|
+
domainMappingService: asFunction(
|
|
31
|
+
function domainMappingServiceFactory(em: EntityManager) {
|
|
32
|
+
// Resolve the cache lazily so registrations from `bootstrap(container)` (which runs after
|
|
33
|
+
// module DI registrars) are visible. Awilix CLASSIC mode would fail to resolve a
|
|
34
|
+
// destructured `cache` param if the key is not yet registered; using `hasRegistration`
|
|
35
|
+
// keeps construction safe for CLI/test contexts where `cache` may never be wired.
|
|
36
|
+
let cacheService: DomainCacheService | undefined
|
|
37
|
+
try {
|
|
38
|
+
if (container.hasRegistration('cache')) {
|
|
39
|
+
const candidate = container.resolve('cache')
|
|
40
|
+
if (isDomainCacheService(candidate)) cacheService = candidate
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
cacheService = undefined
|
|
44
|
+
}
|
|
45
|
+
return new DomainMappingService(em, cacheService ? { cacheService } : undefined)
|
|
46
|
+
},
|
|
47
|
+
).scoped(),
|
|
48
|
+
})
|
|
15
49
|
}
|
|
@@ -17,8 +17,15 @@ const events = [
|
|
|
17
17
|
{ id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },
|
|
18
18
|
{ id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },
|
|
19
19
|
{ id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },
|
|
20
|
-
{ id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },
|
|
21
20
|
{ id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },
|
|
21
|
+
// Custom domain mapping lifecycle (see .ai/specs/2026-04-08-portal-custom-domain-routing.md)
|
|
22
|
+
{ id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },
|
|
23
|
+
{ id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
|
|
24
|
+
{ id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
|
|
25
|
+
{ id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
|
|
26
|
+
{ id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
|
|
27
|
+
{ id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },
|
|
28
|
+
{ id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
|
|
22
29
|
] as const
|
|
23
30
|
|
|
24
31
|
export const eventsConfig = createModuleEvents({
|
|
@@ -192,12 +192,90 @@
|
|
|
192
192
|
"customer_accounts.admin.unverified": "No",
|
|
193
193
|
"customer_accounts.admin.users": "Users",
|
|
194
194
|
"customer_accounts.admin.verified": "Yes",
|
|
195
|
+
"customer_accounts.domainMapping.aRecordInstruction": "For an apex domain, add an A record for {hostname} pointing to {target}",
|
|
196
|
+
"customer_accounts.domainMapping.alreadyClaimed": "This domain is not available. Please choose a different hostname.",
|
|
197
|
+
"customer_accounts.domainMapping.autoVerify.checking": "Automatically checking every 5 minutes",
|
|
198
|
+
"customer_accounts.domainMapping.autoVerify.lastChecked": "Last checked: {time}",
|
|
199
|
+
"customer_accounts.domainMapping.autoVerify.nextCheck": "Next check in ~{minutes} minutes",
|
|
200
|
+
"customer_accounts.domainMapping.changeDomain": "Change Domain",
|
|
201
|
+
"customer_accounts.domainMapping.changeDomain.description": "Your current domain ({hostname}) will stay active until the new domain is fully set up. No downtime for your customers.",
|
|
202
|
+
"customer_accounts.domainMapping.changeDomain.replacing": "Replacing {oldHostname} — old domain stays live until this one is ready",
|
|
203
|
+
"customer_accounts.domainMapping.cnameInstruction": "Add a CNAME record for {hostname} pointing to {target} in your DNS provider",
|
|
204
|
+
"customer_accounts.domainMapping.copyTarget": "Copy",
|
|
205
|
+
"customer_accounts.domainMapping.copyTarget.copied": "Copied",
|
|
206
|
+
"customer_accounts.domainMapping.description": "Map your own domain to the customer portal",
|
|
207
|
+
"customer_accounts.domainMapping.diagnostics.detectedRecords": "Detected DNS records:",
|
|
208
|
+
"customer_accounts.domainMapping.diagnostics.noRecord": "No CNAME or A record found for {hostname}",
|
|
209
|
+
"customer_accounts.domainMapping.diagnostics.proxyDetected": "Your DNS uses a proxy ({proxy}). Either disable the proxy or configure it to forward traffic to our platform.",
|
|
210
|
+
"customer_accounts.domainMapping.diagnostics.suggestion": "Suggested fix:",
|
|
211
|
+
"customer_accounts.domainMapping.diagnostics.timeout": "DNS lookup timed out — this may be a temporary issue",
|
|
212
|
+
"customer_accounts.domainMapping.diagnostics.wrongARecordTarget": "Your A record points to {detected} instead of {expected}",
|
|
213
|
+
"customer_accounts.domainMapping.diagnostics.wrongCnameTarget": "Your CNAME record points to {detected} instead of {expected}",
|
|
214
|
+
"customer_accounts.domainMapping.dialog.cancel": "Cancel",
|
|
215
|
+
"customer_accounts.domainMapping.dialog.change.submit": "Start replacement",
|
|
216
|
+
"customer_accounts.domainMapping.dialog.change.title": "Change custom domain",
|
|
217
|
+
"customer_accounts.domainMapping.dialog.register.submit": "Register",
|
|
218
|
+
"customer_accounts.domainMapping.dialog.register.title": "Register custom domain",
|
|
219
|
+
"customer_accounts.domainMapping.dns.diagnostics.detected": "Detected target",
|
|
220
|
+
"customer_accounts.domainMapping.dns.diagnostics.expected": "Expected target",
|
|
221
|
+
"customer_accounts.domainMapping.dns.diagnostics.title": "DNS configuration issue",
|
|
222
|
+
"customer_accounts.domainMapping.dns.recheckButton": "Re-check DNS",
|
|
223
|
+
"customer_accounts.domainMapping.dnsProviderHelp": "Need help? See instructions for:",
|
|
224
|
+
"customer_accounts.domainMapping.dnsProviderHelp.cloudflare": "Cloudflare",
|
|
225
|
+
"customer_accounts.domainMapping.dnsProviderHelp.godaddy": "GoDaddy",
|
|
226
|
+
"customer_accounts.domainMapping.dnsProviderHelp.googleDomains": "Google Domains",
|
|
227
|
+
"customer_accounts.domainMapping.dnsProviderHelp.namecheap": "Namecheap",
|
|
228
|
+
"customer_accounts.domainMapping.dnsProviderHelp.other": "Other providers",
|
|
229
|
+
"customer_accounts.domainMapping.empty.cta": "Register Domain",
|
|
230
|
+
"customer_accounts.domainMapping.empty.description": "Connect your own domain so customers can reach the portal at a branded URL.",
|
|
231
|
+
"customer_accounts.domainMapping.empty.title": "No custom domain yet",
|
|
232
|
+
"customer_accounts.domainMapping.error.load": "Could not load custom-domain configuration",
|
|
233
|
+
"customer_accounts.domainMapping.errors.required": "Domain is required",
|
|
234
|
+
"customer_accounts.domainMapping.hostname.label": "Domain",
|
|
235
|
+
"customer_accounts.domainMapping.hostname.placeholder": "e.g., shop.yourdomain.com",
|
|
236
|
+
"customer_accounts.domainMapping.hostname.validation.invalid": "Enter a valid domain name (no protocol, path, or port)",
|
|
237
|
+
"customer_accounts.domainMapping.loading": "Loading domain configuration…",
|
|
238
|
+
"customer_accounts.domainMapping.orgSwitcher.label": "Organization",
|
|
239
|
+
"customer_accounts.domainMapping.preview": "Test Domain",
|
|
240
|
+
"customer_accounts.domainMapping.preview.description": "Opens your portal on the custom domain in a new tab to verify it's working correctly",
|
|
241
|
+
"customer_accounts.domainMapping.preview.notReady": "Domain is not yet active. Complete the setup steps first.",
|
|
242
|
+
"customer_accounts.domainMapping.propagationNote": "DNS changes can take up to 48 hours to propagate. The system checks automatically — you'll be notified when it's ready.",
|
|
243
|
+
"customer_accounts.domainMapping.registered": "Domain registered successfully",
|
|
244
|
+
"customer_accounts.domainMapping.removeConfirm": "Are you sure? Customers using this domain will no longer be able to reach your portal.",
|
|
245
|
+
"customer_accounts.domainMapping.removeConfirm.hasReplacement": "This will also cancel the pending replacement domain ({hostname}).",
|
|
246
|
+
"customer_accounts.domainMapping.removeConfirm.title": "Remove custom domain?",
|
|
247
|
+
"customer_accounts.domainMapping.removeDomain": "Remove Domain",
|
|
248
|
+
"customer_accounts.domainMapping.removed": "Domain removed",
|
|
249
|
+
"customer_accounts.domainMapping.replacement.label": "Replacement (waiting to activate)",
|
|
250
|
+
"customer_accounts.domainMapping.status.active": "Active",
|
|
251
|
+
"customer_accounts.domainMapping.status.dns_failed": "DNS issue — see details below",
|
|
252
|
+
"customer_accounts.domainMapping.status.pending": "Checking DNS...",
|
|
253
|
+
"customer_accounts.domainMapping.status.tls_failed": "SSL certificate issue — we're retrying",
|
|
254
|
+
"customer_accounts.domainMapping.status.verified": "Setting up SSL certificate...",
|
|
255
|
+
"customer_accounts.domainMapping.stepper.step1": "Register Domain",
|
|
256
|
+
"customer_accounts.domainMapping.stepper.step2": "Configure DNS",
|
|
257
|
+
"customer_accounts.domainMapping.stepper.step3": "SSL Certificate",
|
|
258
|
+
"customer_accounts.domainMapping.stepper.step4": "Live",
|
|
259
|
+
"customer_accounts.domainMapping.title": "Custom Domain",
|
|
260
|
+
"customer_accounts.domainMapping.tls.diagnostics.operatorNote": "We are retrying automatically. If this persists, contact platform support — your DNS is fine, this is on our side.",
|
|
261
|
+
"customer_accounts.domainMapping.tls.diagnostics.retryCount": "Retry attempts: {count}",
|
|
262
|
+
"customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
|
|
263
|
+
"customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
|
|
264
|
+
"customer_accounts.domainMapping.verifyNow": "Check Now",
|
|
195
265
|
"customer_accounts.nav.group": "Customers",
|
|
196
266
|
"customer_accounts.nav.role_create": "Create Role",
|
|
197
267
|
"customer_accounts.nav.role_detail": "Role Detail",
|
|
198
268
|
"customer_accounts.nav.roles": "Kundenrollen",
|
|
199
269
|
"customer_accounts.nav.user_detail": "User Detail",
|
|
200
270
|
"customer_accounts.nav.users": "Kundenbenutzer",
|
|
271
|
+
"customer_accounts.notifications.domain_mapping.activated.body": "Your custom domain is now active and serving traffic.",
|
|
272
|
+
"customer_accounts.notifications.domain_mapping.activated.title": "Custom domain is live",
|
|
273
|
+
"customer_accounts.notifications.domain_mapping.dns_failed.body": "DNS verification failed for your custom domain. {reason}",
|
|
274
|
+
"customer_accounts.notifications.domain_mapping.dns_failed.title": "DNS verification failed",
|
|
275
|
+
"customer_accounts.notifications.domain_mapping.tls_failed.body": "SSL certificate provisioning is having trouble for your custom domain. We're retrying — no action needed yet.",
|
|
276
|
+
"customer_accounts.notifications.domain_mapping.tls_failed.title": "SSL provisioning issue",
|
|
277
|
+
"customer_accounts.notifications.domain_mapping.verified.body": "Your custom domain has been verified. SSL certificate is being provisioned.",
|
|
278
|
+
"customer_accounts.notifications.domain_mapping.verified.title": "Custom domain verified",
|
|
201
279
|
"customer_accounts.notifications.user.locked.body": "Customer account was locked due to too many failed login attempts.",
|
|
202
280
|
"customer_accounts.notifications.user.locked.title": "Customer account locked",
|
|
203
281
|
"customer_accounts.notifications.user.signup.body": "A new customer account was created.",
|