44 "context"
55 "errors"
66 "fmt"
7+ "net"
8+ "strings"
9+ "sync/atomic"
710
811 "github.com/google/uuid"
912
@@ -22,12 +25,10 @@ const defaultTenantName = "UTMStack"
2225var ErrBootstrapPasswordRequired = errors .New (
2326 "UTMSTACK_ADMIN_PASSWORD is required to create the initial administrator" )
2427
25- var ErrBootstrapDomainRequired = errors .New (
26- "UTMSTACK_DEFAULT_DOMAIN is required to create the default tenant" )
27-
2828type bootstrapUsecase struct {
29- repo connectors.TenantRepository
30- admin connectors.UserProvisioner
29+ repo connectors.TenantRepository
30+ admin connectors.UserProvisioner
31+ healed atomic.Bool
3132}
3233
3334func NewBootstrapUsecase (repo connectors.TenantRepository , admin connectors.UserProvisioner ) connectors.BootstrapUsecase {
@@ -46,15 +47,15 @@ func (u *bootstrapUsecase) EnsureDefaultTenant(ctx context.Context, adminEmail,
4647 return false , fmt .Errorf ("looking up the default tenant: %w" , err )
4748 }
4849 if existing != nil {
50+ if existing .Domain != "" {
51+ u .healed .Store (true )
52+ }
4953 return false , nil
5054 }
5155
5256 if adminPassword == "" {
5357 return false , ErrBootstrapPasswordRequired
5458 }
55- if tenantDomain == "" {
56- return false , ErrBootstrapDomainRequired
57- }
5859
5960 t := & domain.Tenant {
6061 ID : defaultTenantID ,
@@ -65,6 +66,9 @@ func (u *bootstrapUsecase) EnsureDefaultTenant(ctx context.Context, adminEmail,
6566 if err := u .repo .Create (all , t ); err != nil {
6667 return false , fmt .Errorf ("creating the default tenant: %w" , err )
6768 }
69+ if tenantDomain != "" {
70+ u .healed .Store (true )
71+ }
6872
6973 if err := provisionAdmin (ctx , u .admin , t .ID , adminEmail , adminPassword , false ); err != nil {
7074 if delErr := u .repo .Delete (all , t .ID ); delErr != nil {
@@ -75,6 +79,48 @@ func (u *bootstrapUsecase) EnsureDefaultTenant(ctx context.Context, adminEmail,
7579 return true , nil
7680}
7781
82+ // TryHealDefaultDomain stamps the default tenant's Domain from the first
83+ // request's Host when UTMSTACK_DEFAULT_DOMAIN was not set at install time.
84+ // Runs at most once per process (atomic fast path), and no-ops once the row
85+ // already has a domain.
86+ func (u * bootstrapUsecase ) TryHealDefaultDomain (ctx context.Context , host string ) error {
87+ if u .healed .Load () {
88+ return nil
89+ }
90+ host = normalizeHealHost (host )
91+ if host == "" {
92+ return nil
93+ }
94+ all := tenancy .WithAllTenants (ctx )
95+ t , err := u .repo .FindByID (all , defaultTenantID )
96+ if err != nil || t == nil {
97+ return err
98+ }
99+ if t .Domain != "" {
100+ u .healed .Store (true )
101+ return nil
102+ }
103+ t .Domain = host
104+ if err := u .repo .Update (all , t ); err != nil {
105+ return err
106+ }
107+ u .healed .Store (true )
108+ return nil
109+ }
110+
111+ func normalizeHealHost (raw string ) string {
112+ // X-Forwarded-Host may carry a comma-separated chain; the first hop is the
113+ // original client-facing hostname.
114+ if i := strings .IndexByte (raw , ',' ); i >= 0 {
115+ raw = raw [:i ]
116+ }
117+ raw = strings .TrimSpace (raw )
118+ if h , _ , err := net .SplitHostPort (raw ); err == nil {
119+ raw = h
120+ }
121+ return strings .ToLower (raw )
122+ }
123+
78124func provisionAdmin (ctx context.Context , admin connectors.UserProvisioner , tenantID uuid.UUID , email , password string , invite bool ) error {
79125 if admin == nil {
80126 return nil
0 commit comments