@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.
Files changed (212) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/domain_mapping/index.js +31 -0
  3. package/dist/generated/entities/domain_mapping/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +2 -1
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +16 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/salesUi.js +18 -17
  9. package/dist/helpers/integration/salesUi.js.map +2 -2
  10. package/dist/modules/customer_accounts/acl.js +2 -1
  11. package/dist/modules/customer_accounts/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
  13. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
  14. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
  15. package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
  16. package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
  17. package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
  18. package/dist/modules/customer_accounts/api/domain-check.js +70 -0
  19. package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
  20. package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
  21. package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
  22. package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
  23. package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
  24. package/dist/modules/customer_accounts/api/login.js +14 -3
  25. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
  27. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
  29. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/signup.js +32 -5
  31. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  32. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
  33. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
  34. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
  35. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
  36. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
  37. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
  38. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
  39. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
  40. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
  41. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
  42. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
  43. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
  44. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
  45. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
  46. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
  47. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
  48. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
  49. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
  50. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
  51. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
  52. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
  53. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
  54. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
  55. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
  56. package/dist/modules/customer_accounts/data/enrichers.js +78 -12
  57. package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
  58. package/dist/modules/customer_accounts/data/entities.js +85 -2
  59. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  60. package/dist/modules/customer_accounts/data/guards.js +130 -0
  61. package/dist/modules/customer_accounts/data/guards.js.map +7 -0
  62. package/dist/modules/customer_accounts/data/validators.js +19 -0
  63. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  64. package/dist/modules/customer_accounts/di.js +23 -1
  65. package/dist/modules/customer_accounts/di.js.map +2 -2
  66. package/dist/modules/customer_accounts/events.js +9 -2
  67. package/dist/modules/customer_accounts/events.js.map +2 -2
  68. package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
  69. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  70. package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
  71. package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
  72. package/dist/modules/customer_accounts/lib/hostname.js +55 -0
  73. package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
  74. package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
  75. package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
  76. package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
  77. package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
  78. package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
  79. package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
  80. package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
  81. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
  82. package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
  83. package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
  84. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
  85. package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
  86. package/dist/modules/customer_accounts/notifications.client.js +76 -0
  87. package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +76 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
  91. package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
  92. package/dist/modules/customer_accounts/setup.js +48 -1
  93. package/dist/modules/customer_accounts/setup.js.map +2 -2
  94. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
  95. package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
  96. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
  97. package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
  98. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
  99. package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
  100. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
  101. package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
  102. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
  103. package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
  104. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
  105. package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
  106. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
  107. package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
  108. package/dist/modules/customers/api/companies/route.js +4 -9
  109. package/dist/modules/customers/api/companies/route.js.map +2 -2
  110. package/dist/modules/customers/api/deals/route.js +4 -9
  111. package/dist/modules/customers/api/deals/route.js.map +2 -2
  112. package/dist/modules/customers/api/people/route.js +4 -9
  113. package/dist/modules/customers/api/people/route.js.map +2 -2
  114. package/dist/modules/customers/api/utils.js +0 -12
  115. package/dist/modules/customers/api/utils.js.map +2 -2
  116. package/dist/modules/customers/backend/customers/companies/page.js +361 -284
  117. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  118. package/dist/modules/customers/backend/customers/deals/page.js +378 -473
  119. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  120. package/dist/modules/customers/backend/customers/people/page.js +368 -286
  121. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  122. package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
  123. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  124. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
  125. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
  126. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
  127. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
  128. package/dist/modules/customers/widgets/injection-table.js +6 -0
  129. package/dist/modules/customers/widgets/injection-table.js.map +2 -2
  130. package/dist/modules/perspectives/services/perspectiveService.js +16 -2
  131. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  132. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
  133. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
  134. package/dist/modules/query_index/lib/engine.js +31 -8
  135. package/dist/modules/query_index/lib/engine.js.map +2 -2
  136. package/generated/entities/domain_mapping/index.ts +14 -0
  137. package/generated/entities.ids.generated.ts +2 -1
  138. package/generated/entity-fields-registry.ts +16 -0
  139. package/package.json +5 -5
  140. package/src/helpers/integration/salesUi.ts +27 -23
  141. package/src/modules/customer_accounts/acl.ts +1 -0
  142. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
  143. package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
  144. package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
  145. package/src/modules/customer_accounts/api/domain-check.ts +83 -0
  146. package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
  147. package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
  148. package/src/modules/customer_accounts/api/login.ts +14 -3
  149. package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
  150. package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
  151. package/src/modules/customer_accounts/api/signup.ts +36 -5
  152. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
  153. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
  154. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
  155. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
  156. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
  157. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
  158. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
  159. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
  160. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
  161. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
  162. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
  163. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
  164. package/src/modules/customer_accounts/data/enrichers.ts +95 -12
  165. package/src/modules/customer_accounts/data/entities.ts +80 -1
  166. package/src/modules/customer_accounts/data/guards.ts +155 -0
  167. package/src/modules/customer_accounts/data/validators.ts +26 -0
  168. package/src/modules/customer_accounts/di.ts +35 -1
  169. package/src/modules/customer_accounts/events.ts +8 -1
  170. package/src/modules/customer_accounts/i18n/de.json +78 -0
  171. package/src/modules/customer_accounts/i18n/en.json +78 -0
  172. package/src/modules/customer_accounts/i18n/es.json +78 -0
  173. package/src/modules/customer_accounts/i18n/pl.json +78 -0
  174. package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
  175. package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
  176. package/src/modules/customer_accounts/lib/hostname.ts +76 -0
  177. package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
  178. package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
  179. package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
  180. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
  181. package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
  182. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
  183. package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
  184. package/src/modules/customer_accounts/notifications.client.ts +76 -0
  185. package/src/modules/customer_accounts/notifications.ts +76 -0
  186. package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
  187. package/src/modules/customer_accounts/setup.ts +81 -1
  188. package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
  189. package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
  190. package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
  191. package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
  192. package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
  193. package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
  194. package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
  195. package/src/modules/customers/api/companies/route.ts +4 -9
  196. package/src/modules/customers/api/deals/route.ts +4 -9
  197. package/src/modules/customers/api/people/route.ts +4 -9
  198. package/src/modules/customers/api/utils.ts +0 -14
  199. package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
  200. package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
  201. package/src/modules/customers/backend/customers/people/page.tsx +331 -240
  202. package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
  203. package/src/modules/customers/i18n/de.json +31 -0
  204. package/src/modules/customers/i18n/en.json +31 -0
  205. package/src/modules/customers/i18n/es.json +31 -0
  206. package/src/modules/customers/i18n/pl.json +31 -0
  207. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
  208. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
  209. package/src/modules/customers/widgets/injection-table.ts +6 -0
  210. package/src/modules/perspectives/services/perspectiveService.ts +30 -2
  211. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
  212. package/src/modules/query_index/lib/engine.ts +46 -8
@@ -0,0 +1,56 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { domainStatusMap } from './domainStatusMap'
7
+ import type { DomainMappingRow } from './types'
8
+
9
+ export type StatusBadgeSectionProps = {
10
+ mapping: DomainMappingRow
11
+ }
12
+
13
+ function formatTime(value: string | null, fallback: string): string {
14
+ if (!value) return fallback
15
+ const date = new Date(value)
16
+ if (Number.isNaN(date.getTime())) return fallback
17
+ return date.toLocaleString()
18
+ }
19
+
20
+ export function DefaultStatusBadge({ mapping }: StatusBadgeSectionProps) {
21
+ const t = useT()
22
+ const variant = domainStatusMap[mapping.status]
23
+ const label = t(
24
+ `customer_accounts.domainMapping.status.${mapping.status}`,
25
+ mapping.status,
26
+ )
27
+ const lastChecked = formatTime(
28
+ mapping.lastDnsCheckAt,
29
+ t('customer_accounts.domainMapping.autoVerify.checking', 'Automatically checking every 5 minutes'),
30
+ )
31
+ const showAutoCheck = mapping.status === 'pending' || mapping.status === 'dns_failed'
32
+
33
+ return (
34
+ <div className="flex flex-wrap items-center gap-3">
35
+ <StatusBadge variant={variant} dot>
36
+ {label}
37
+ </StatusBadge>
38
+ {mapping.lastDnsCheckAt ? (
39
+ <span className="text-xs text-muted-foreground">
40
+ {t('customer_accounts.domainMapping.autoVerify.lastChecked', 'Last checked: {time}', {
41
+ time: lastChecked,
42
+ })}
43
+ </span>
44
+ ) : null}
45
+ {showAutoCheck ? (
46
+ <span className="text-xs text-muted-foreground">
47
+ {t('customer_accounts.domainMapping.autoVerify.nextCheck', 'Next check in ~{minutes} minutes', {
48
+ minutes: '5',
49
+ })}
50
+ </span>
51
+ ) : null}
52
+ </div>
53
+ )
54
+ }
55
+
56
+ export default DefaultStatusBadge
@@ -0,0 +1,69 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Check } from 'lucide-react'
5
+ import { cn } from '@open-mercato/shared/lib/utils'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import type { DomainStatus } from '@open-mercato/core/modules/customer_accounts/data/entities'
8
+
9
+ export type StepperProps = {
10
+ status: DomainStatus | null
11
+ }
12
+
13
+ function activeStep(status: DomainStatus | null): number {
14
+ if (!status) return 1
15
+ if (status === 'pending' || status === 'dns_failed') return 2
16
+ if (status === 'verified' || status === 'tls_failed') return 3
17
+ if (status === 'active') return 4
18
+ return 1
19
+ }
20
+
21
+ export function DefaultStepper({ status }: StepperProps) {
22
+ const t = useT()
23
+ const current = activeStep(status)
24
+ const steps: Array<{ idx: number; label: string }> = [
25
+ { idx: 1, label: t('customer_accounts.domainMapping.stepper.step1', 'Register Domain') },
26
+ { idx: 2, label: t('customer_accounts.domainMapping.stepper.step2', 'Configure DNS') },
27
+ { idx: 3, label: t('customer_accounts.domainMapping.stepper.step3', 'SSL Certificate') },
28
+ { idx: 4, label: t('customer_accounts.domainMapping.stepper.step4', 'Live') },
29
+ ]
30
+
31
+ return (
32
+ <ol className="flex flex-wrap items-center gap-3 text-sm">
33
+ {steps.map((step, index) => {
34
+ const isDone = step.idx < current
35
+ const isCurrent = step.idx === current
36
+ return (
37
+ <li key={step.idx} className="flex items-center gap-3">
38
+ <div className="flex items-center gap-2">
39
+ <span
40
+ className={cn(
41
+ 'flex h-7 w-7 items-center justify-center rounded-full border text-xs font-medium',
42
+ isDone && 'border-status-success-icon bg-status-success-icon text-status-success-foreground',
43
+ isCurrent && 'border-primary bg-primary/10 text-primary',
44
+ !isDone && !isCurrent && 'border-border bg-background text-muted-foreground',
45
+ )}
46
+ aria-current={isCurrent ? 'step' : undefined}
47
+ >
48
+ {isDone ? <Check className="h-4 w-4" aria-hidden /> : step.idx}
49
+ </span>
50
+ <span
51
+ className={cn(
52
+ 'whitespace-nowrap',
53
+ isCurrent ? 'font-medium text-foreground' : 'text-muted-foreground',
54
+ )}
55
+ >
56
+ {step.label}
57
+ </span>
58
+ </div>
59
+ {index < steps.length - 1 ? (
60
+ <span className="hidden h-px w-8 bg-border sm:block" aria-hidden />
61
+ ) : null}
62
+ </li>
63
+ )
64
+ })}
65
+ </ol>
66
+ )
67
+ }
68
+
69
+ export default DefaultStepper
@@ -0,0 +1,10 @@
1
+ import type { StatusMap } from '@open-mercato/ui/primitives/status-badge'
2
+ import type { DomainStatus } from '@open-mercato/core/modules/customer_accounts/data/entities'
3
+
4
+ export const domainStatusMap: StatusMap<DomainStatus> = {
5
+ pending: 'info',
6
+ verified: 'info',
7
+ active: 'success',
8
+ dns_failed: 'error',
9
+ tls_failed: 'warning',
10
+ }
@@ -0,0 +1,31 @@
1
+ import type { DomainStatus } from '@open-mercato/core/modules/customer_accounts/data/entities'
2
+
3
+ export type DomainMappingRow = {
4
+ id: string
5
+ hostname: string
6
+ organizationId: string
7
+ tenantId: string
8
+ provider: string
9
+ status: DomainStatus
10
+ verifiedAt: string | null
11
+ lastDnsCheckAt: string | null
12
+ dnsFailureReason: string | null
13
+ tlsFailureReason: string | null
14
+ tlsRetryCount: number
15
+ cnameTarget: string | null
16
+ aRecordTarget: string | null
17
+ createdAt: string
18
+ updatedAt: string | null
19
+ }
20
+
21
+ export type DomainConfig = {
22
+ cnameTarget: string | null
23
+ aRecordTarget: string | null
24
+ }
25
+
26
+ export type DomainListResponse = {
27
+ ok: boolean
28
+ domainMappings?: DomainMappingRow[]
29
+ config?: DomainConfig
30
+ error?: string
31
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+
3
+ const globeIcon = React.createElement(
4
+ 'svg',
5
+ { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
6
+ React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
7
+ React.createElement('path', { d: 'M2 12h20' }),
8
+ React.createElement('path', { d: 'M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z' }),
9
+ )
10
+
11
+ export const metadata = {
12
+ requireAuth: true,
13
+ requireFeatures: ['customer_accounts.domain.manage'],
14
+ pageTitle: 'Custom Domain',
15
+ pageTitleKey: 'customer_accounts.domainMapping.title',
16
+ pageGroup: 'Module Configs',
17
+ pageGroupKey: 'settings.sections.moduleConfigs',
18
+ pageOrder: 60,
19
+ icon: globeIcon,
20
+ pageContext: 'settings' as const,
21
+ breadcrumb: [
22
+ { label: 'Custom Domain', labelKey: 'customer_accounts.domainMapping.title' },
23
+ ],
24
+ }
25
+
26
+ export default metadata
@@ -0,0 +1,411 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useRouter, useSearchParams } from 'next/navigation'
5
+ import { Globe } from 'lucide-react'
6
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
+ import { FormHeader } from '@open-mercato/ui/backend/forms'
8
+ import { EmptyState } from '@open-mercato/ui/backend/EmptyState'
9
+ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
10
+ import { Button } from '@open-mercato/ui/primitives/button'
11
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
12
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
13
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
14
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
15
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
16
+ import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
17
+ import { OrgSwitcher, type OrgOption } from './components/OrgSwitcher'
18
+ import { DomainPanel } from './components/DomainPanel'
19
+ import { RegisterDialog } from './components/RegisterDialog'
20
+ import type { DomainListResponse, DomainMappingRow, DomainConfig } from './components/types'
21
+
22
+ const STATE_LOADING = 'loading'
23
+ const STATE_LOADED = 'loaded'
24
+ const STATE_ERROR = 'error'
25
+ type LoadState = typeof STATE_LOADING | typeof STATE_LOADED | typeof STATE_ERROR
26
+
27
+ type OrgSwitcherNode = {
28
+ id: string
29
+ name: string
30
+ depth: number
31
+ selectable: boolean
32
+ children: OrgSwitcherNode[]
33
+ }
34
+
35
+ type OrgSwitcherResponse = {
36
+ items?: OrgSwitcherNode[]
37
+ selectedId?: string | null
38
+ }
39
+
40
+ function flattenOrgNodes(nodes: OrgSwitcherNode[] | undefined, acc: OrgOption[] = [], depth = 0): OrgOption[] {
41
+ if (!nodes) return acc
42
+ for (const node of nodes) {
43
+ if (node.selectable) {
44
+ acc.push({ id: node.id, label: node.name, depth })
45
+ }
46
+ flattenOrgNodes(node.children, acc, depth + 1)
47
+ }
48
+ return acc
49
+ }
50
+
51
+ export default function CustomerDomainSettingsPage() {
52
+ const t = useT()
53
+ const router = useRouter()
54
+ const searchParams = useSearchParams()
55
+
56
+ const [orgOptions, setOrgOptions] = React.useState<OrgOption[]>([])
57
+ const [defaultOrgId, setDefaultOrgId] = React.useState<string | null>(null)
58
+ const [orgsLoaded, setOrgsLoaded] = React.useState(false)
59
+
60
+ const urlOrgId = searchParams?.get('org') ?? null
61
+ const selectedOrgId = urlOrgId ?? defaultOrgId
62
+
63
+ const [mappings, setMappings] = React.useState<DomainMappingRow[]>([])
64
+ const [config, setConfig] = React.useState<DomainConfig>({ cnameTarget: null, aRecordTarget: null })
65
+ const [loadState, setLoadState] = React.useState<LoadState>(STATE_LOADING)
66
+ const [reloadToken, setReloadToken] = React.useState(0)
67
+ const [registerOpen, setRegisterOpen] = React.useState(false)
68
+ const [registerMode, setRegisterMode] = React.useState<'register' | 'change'>('register')
69
+ const [replaceTargetId, setReplaceTargetId] = React.useState<string | null>(null)
70
+ const [isMutating, setIsMutating] = React.useState(false)
71
+
72
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
73
+ const { runMutation } = useGuardedMutation<{ entityType: string; entityId?: string }>({
74
+ contextId: 'customer_accounts:domain-settings',
75
+ })
76
+
77
+ const runMutationWithContext = React.useCallback(
78
+ async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
79
+ return runMutation({
80
+ operation,
81
+ mutationPayload,
82
+ context: { entityType: 'customer_accounts:domain_mapping' },
83
+ })
84
+ },
85
+ [runMutation],
86
+ )
87
+
88
+ const refresh = React.useCallback(() => {
89
+ setReloadToken((token) => token + 1)
90
+ }, [])
91
+
92
+ React.useEffect(() => {
93
+ let cancelled = false
94
+ async function loadOrgs() {
95
+ try {
96
+ const call = await apiCall<OrgSwitcherResponse>('/api/directory/organization-switcher')
97
+ if (cancelled) return
98
+ if (!call.ok || !call.result) {
99
+ setOrgOptions([])
100
+ setDefaultOrgId(null)
101
+ return
102
+ }
103
+ const flat = flattenOrgNodes(call.result.items, [], 0)
104
+ setOrgOptions(flat)
105
+ const fallback = call.result.selectedId ?? (flat[0]?.id ?? null)
106
+ setDefaultOrgId(fallback)
107
+ } catch {
108
+ if (!cancelled) {
109
+ setOrgOptions([])
110
+ setDefaultOrgId(null)
111
+ }
112
+ } finally {
113
+ if (!cancelled) setOrgsLoaded(true)
114
+ }
115
+ }
116
+ loadOrgs()
117
+ return () => {
118
+ cancelled = true
119
+ }
120
+ }, [])
121
+
122
+ React.useEffect(() => {
123
+ if (!orgsLoaded) return
124
+ if (!selectedOrgId) {
125
+ setMappings([])
126
+ setConfig({ cnameTarget: null, aRecordTarget: null })
127
+ setLoadState(STATE_LOADED)
128
+ return
129
+ }
130
+ let cancelled = false
131
+ async function load() {
132
+ setLoadState(STATE_LOADING)
133
+ try {
134
+ const call = await apiCall<DomainListResponse>(
135
+ `/api/customer_accounts/admin/domain-mappings?organizationId=${encodeURIComponent(selectedOrgId!)}`,
136
+ )
137
+ if (cancelled) return
138
+ if (!call.ok || !call.result?.ok) {
139
+ setLoadState(STATE_ERROR)
140
+ return
141
+ }
142
+ setMappings(Array.isArray(call.result.domainMappings) ? call.result.domainMappings : [])
143
+ setConfig(call.result.config ?? { cnameTarget: null, aRecordTarget: null })
144
+ setLoadState(STATE_LOADED)
145
+ } catch {
146
+ if (!cancelled) setLoadState(STATE_ERROR)
147
+ }
148
+ }
149
+ load()
150
+ return () => {
151
+ cancelled = true
152
+ }
153
+ }, [orgsLoaded, selectedOrgId, reloadToken])
154
+
155
+ useAppEvent('customer_accounts.domain_mapping.*', () => {
156
+ refresh()
157
+ }, [refresh])
158
+
159
+ const orderedMappings = React.useMemo(() => {
160
+ if (mappings.length === 0) return []
161
+ const sorted = [...mappings].sort((a, b) => {
162
+ const aActive = a.status === 'active' ? 0 : 1
163
+ const bActive = b.status === 'active' ? 0 : 1
164
+ if (aActive !== bActive) return aActive - bActive
165
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
166
+ })
167
+ return sorted
168
+ }, [mappings])
169
+
170
+ const primaryActiveId = React.useMemo(() => {
171
+ const active = orderedMappings.find((m) => m.status === 'active')
172
+ if (active) return active.id
173
+ return orderedMappings[0]?.id ?? null
174
+ }, [orderedMappings])
175
+
176
+ const handleOrgChange = React.useCallback(
177
+ (next: string) => {
178
+ const params = new URLSearchParams(searchParams?.toString() ?? '')
179
+ if (next) params.set('org', next)
180
+ else params.delete('org')
181
+ const qs = params.toString()
182
+ router.replace(qs ? `?${qs}` : '?', { scroll: false })
183
+ },
184
+ [router, searchParams],
185
+ )
186
+
187
+ const handleOpenRegister = React.useCallback(() => {
188
+ setRegisterMode('register')
189
+ setReplaceTargetId(null)
190
+ setRegisterOpen(true)
191
+ }, [])
192
+
193
+ const handleOpenChange = React.useCallback((mappingId: string) => {
194
+ setRegisterMode('change')
195
+ setReplaceTargetId(mappingId)
196
+ setRegisterOpen(true)
197
+ }, [])
198
+
199
+ const handleSubmitRegister = React.useCallback(
200
+ async (hostname: string) => {
201
+ if (!selectedOrgId) {
202
+ throw new Error(t('customer_accounts.domainMapping.error.load', 'Could not load custom-domain configuration'))
203
+ }
204
+ setIsMutating(true)
205
+ try {
206
+ await runMutationWithContext(async () => {
207
+ const body: Record<string, unknown> = {
208
+ hostname,
209
+ organizationId: selectedOrgId,
210
+ }
211
+ if (registerMode === 'change' && replaceTargetId) {
212
+ body.replacesDomainId = replaceTargetId
213
+ }
214
+ const call = await apiCall<{ ok: boolean; error?: string; domainMapping?: DomainMappingRow }>(
215
+ '/api/customer_accounts/admin/domain-mappings',
216
+ {
217
+ method: 'POST',
218
+ headers: { 'content-type': 'application/json' },
219
+ body: JSON.stringify(body),
220
+ },
221
+ )
222
+ if (!call.ok || !call.result?.ok) {
223
+ const message = call.result?.error
224
+ || t('customer_accounts.domainMapping.alreadyClaimed', 'This domain is not available. Please choose a different hostname.')
225
+ throw new Error(message)
226
+ }
227
+ flash(t('customer_accounts.domainMapping.registered', 'Domain registered successfully'), 'success')
228
+ setRegisterOpen(false)
229
+ refresh()
230
+ })
231
+ } finally {
232
+ setIsMutating(false)
233
+ }
234
+ },
235
+ [refresh, registerMode, replaceTargetId, runMutationWithContext, selectedOrgId, t],
236
+ )
237
+
238
+ const handleCheckNow = React.useCallback(
239
+ async (mapping: DomainMappingRow) => {
240
+ setIsMutating(true)
241
+ try {
242
+ await runMutationWithContext(async () => {
243
+ const call = await apiCall<{ ok: boolean; error?: string }>(
244
+ `/api/customer_accounts/admin/domain-mappings/${encodeURIComponent(mapping.id)}/verify`,
245
+ { method: 'POST' },
246
+ )
247
+ if (!call.ok || !call.result?.ok) {
248
+ const message = call.result?.error || t('customer_accounts.domainMapping.error.load', 'Could not load custom-domain configuration')
249
+ flash(message, 'error')
250
+ return
251
+ }
252
+ refresh()
253
+ })
254
+ } finally {
255
+ setIsMutating(false)
256
+ }
257
+ },
258
+ [refresh, runMutationWithContext, t],
259
+ )
260
+
261
+ const handleRetryTls = React.useCallback(
262
+ async (mapping: DomainMappingRow) => {
263
+ setIsMutating(true)
264
+ try {
265
+ await runMutationWithContext(async () => {
266
+ const call = await apiCall<{ ok: boolean; error?: string }>(
267
+ `/api/customer_accounts/admin/domain-mappings/${encodeURIComponent(mapping.id)}/health-check`,
268
+ { method: 'POST' },
269
+ )
270
+ if (!call.ok || !call.result?.ok) {
271
+ const message = call.result?.error || t('customer_accounts.domainMapping.error.load', 'Could not load custom-domain configuration')
272
+ flash(message, 'error')
273
+ return
274
+ }
275
+ refresh()
276
+ })
277
+ } finally {
278
+ setIsMutating(false)
279
+ }
280
+ },
281
+ [refresh, runMutationWithContext, t],
282
+ )
283
+
284
+ const handleRemove = React.useCallback(
285
+ async (mapping: DomainMappingRow) => {
286
+ const replacement = orderedMappings.find(
287
+ (m) => m.id !== mapping.id && m.status !== 'active',
288
+ )
289
+ const text = replacement
290
+ ? t('customer_accounts.domainMapping.removeConfirm.hasReplacement', 'This will also cancel the pending replacement domain ({hostname}).', { hostname: replacement.hostname })
291
+ : t('customer_accounts.domainMapping.removeConfirm', 'Are you sure? Customers using this domain will no longer be able to reach your portal.')
292
+ const confirmed = await confirm({
293
+ title: t('customer_accounts.domainMapping.removeConfirm.title', 'Remove custom domain?'),
294
+ text,
295
+ variant: 'destructive',
296
+ confirmText: t('customer_accounts.domainMapping.removeDomain', 'Remove Domain'),
297
+ })
298
+ if (!confirmed) return
299
+ setIsMutating(true)
300
+ try {
301
+ await runMutationWithContext(async () => {
302
+ const call = await apiCall<{ ok: boolean; error?: string }>(
303
+ `/api/customer_accounts/admin/domain-mappings?id=${encodeURIComponent(mapping.id)}`,
304
+ { method: 'DELETE' },
305
+ )
306
+ if (!call.ok || !call.result?.ok) {
307
+ const message = call.result?.error || t('customer_accounts.domainMapping.error.load', 'Could not load custom-domain configuration')
308
+ flash(message, 'error')
309
+ return
310
+ }
311
+ flash(t('customer_accounts.domainMapping.removed', 'Domain removed'), 'success')
312
+ refresh()
313
+ })
314
+ } finally {
315
+ setIsMutating(false)
316
+ }
317
+ },
318
+ [confirm, orderedMappings, refresh, runMutationWithContext, t],
319
+ )
320
+
321
+ const replaceTargetHostname = React.useMemo(() => {
322
+ if (registerMode !== 'change' || !replaceTargetId) return null
323
+ return orderedMappings.find((m) => m.id === replaceTargetId)?.hostname ?? null
324
+ }, [orderedMappings, registerMode, replaceTargetId])
325
+
326
+ return (
327
+ <Page>
328
+ <div className="px-3 py-3 md:px-6 md:py-4">
329
+ <FormHeader
330
+ mode="detail"
331
+ title={t('customer_accounts.domainMapping.title', 'Custom Domain')}
332
+ subtitle={t('customer_accounts.domainMapping.description', 'Map your own domain to the customer portal')}
333
+ backHref="/backend/customer_accounts/settings"
334
+ backLabel={t('customer_accounts.settings.title', 'Portal Settings')}
335
+ />
336
+ </div>
337
+
338
+ <PageBody className="space-y-6">
339
+ {orgOptions.length > 1 ? (
340
+ <div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border bg-card px-4 py-3">
341
+ <OrgSwitcher
342
+ options={orgOptions}
343
+ selectedId={selectedOrgId}
344
+ onChange={handleOrgChange}
345
+ />
346
+ </div>
347
+ ) : null}
348
+
349
+ {loadState === STATE_LOADING ? (
350
+ <LoadingMessage label={t('customer_accounts.domainMapping.loading', 'Loading domain configuration…')} />
351
+ ) : null}
352
+
353
+ {loadState === STATE_ERROR ? (
354
+ <ErrorMessage
355
+ label={t('customer_accounts.domainMapping.error.load', 'Could not load custom-domain configuration')}
356
+ action={
357
+ <Button type="button" variant="outline" size="sm" onClick={refresh}>
358
+ {t('customer_accounts.domainMapping.verifyNow', 'Check Now')}
359
+ </Button>
360
+ }
361
+ />
362
+ ) : null}
363
+
364
+ {loadState === STATE_LOADED && orderedMappings.length === 0 && selectedOrgId ? (
365
+ <EmptyState
366
+ icon={<Globe className="h-8 w-8" aria-hidden />}
367
+ title={t('customer_accounts.domainMapping.empty.title', 'No custom domain yet')}
368
+ description={t('customer_accounts.domainMapping.empty.description', 'Connect your own domain so customers can reach the portal at a branded URL.')}
369
+ action={{
370
+ label: t('customer_accounts.domainMapping.empty.cta', 'Register Domain'),
371
+ onClick: handleOpenRegister,
372
+ icon: <Globe className="h-4 w-4" aria-hidden />,
373
+ }}
374
+ />
375
+ ) : null}
376
+
377
+ {loadState === STATE_LOADED && orderedMappings.length > 0 ? (
378
+ <div className="space-y-4">
379
+ {orderedMappings.map((mapping) => {
380
+ const isReplacement = mapping.id !== primaryActiveId && mapping.status !== 'active'
381
+ return (
382
+ <DomainPanel
383
+ key={mapping.id}
384
+ mapping={mapping}
385
+ isReplacement={isReplacement}
386
+ cnameTarget={config.cnameTarget}
387
+ aRecordTarget={config.aRecordTarget}
388
+ busy={isMutating}
389
+ onCheckNow={() => handleCheckNow(mapping)}
390
+ onRetryTls={() => handleRetryTls(mapping)}
391
+ onChangeDomain={() => handleOpenChange(mapping.id)}
392
+ onRemove={() => handleRemove(mapping)}
393
+ />
394
+ )
395
+ })}
396
+ </div>
397
+ ) : null}
398
+ </PageBody>
399
+
400
+ <RegisterDialog
401
+ open={registerOpen}
402
+ onOpenChange={setRegisterOpen}
403
+ mode={registerMode}
404
+ currentHostname={replaceTargetHostname}
405
+ onSubmit={handleSubmitRegister}
406
+ />
407
+
408
+ {ConfirmDialogElement}
409
+ </Page>
410
+ )
411
+ }