package db import ( "fmt" "log/slog" "gomail/internal/crypto" "gomail/internal/dkim" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) // Bootstrap creates an initial tenant, domain, and global admin user on first // run — detected by the absence of any global_admin row. Safe to call on // every startup; it's a no-op once bootstrapped. func (db *DB) Bootstrap(hostname, initPassword string, bcryptCost int, mk *crypto.MasterKey) error { var count int err := db.QueryRow("SELECT COUNT(*) FROM users WHERE role = 'global_admin'").Scan(&count) if err != nil { return fmt.Errorf("checking existing admins: %w", err) } if count > 0 { return nil // already bootstrapped } if initPassword == "" { initPassword = "ChangeMe123!" slog.Warn("no admin exists and GOMAIL_ADMIN_INIT_PASSWORD not set — using default, CHANGE IMMEDIATELY", "password", initPassword, "email", "admin@"+hostname) } tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() tenantID := uuid.NewString() if _, err := tx.Exec( `INSERT INTO tenants (id, name, display_name) VALUES (?, ?, ?)`, tenantID, "default", "Default Tenant", ); err != nil { return fmt.Errorf("creating default tenant: %w", err) } domainID := uuid.NewString() dkimSelector := "mail" // Generate a DKIM key pair so outbound mail can be signed immediately — // without this, every message this instance sends would be unsigned // until an admin manually configures one (Phase 8's admin portal will // add key rotation/regeneration; this just ensures a working default). var dkimKeyEnc []byte kp, kpErr := dkim.GenerateKeyPair() if kpErr != nil { slog.Warn("failed to generate DKIM key during bootstrap — outbound mail will be unsigned until one is configured", "err", kpErr) } else { encrypted, encErr := crypto.Encrypt(mk, domainID, "dkim-key", kp.PrivateKeyPEM) if encErr != nil { slog.Warn("failed to encrypt DKIM key during bootstrap", "err", encErr) } else { dkimKeyEnc = encrypted slog.Info("DKIM key generated for default domain — publish this DNS TXT record", "record_name", dkimSelector+"._domainkey."+hostname, "record_value", kp.DNSRecordValue) } } if _, err := tx.Exec( `INSERT INTO domains (id, tenant_id, domain, active, accept_all, dkim_selector, dkim_private_key_enc) VALUES (?, ?, ?, 1, 1, ?, ?)`, domainID, tenantID, hostname, dkimSelector, dkimKeyEnc, ); err != nil { return fmt.Errorf("creating default domain: %w", err) } hash, err := bcrypt.GenerateFromPassword([]byte(initPassword), bcryptCost) if err != nil { return fmt.Errorf("hashing admin password: %w", err) } adminEmail := "admin@" + hostname if _, err := tx.Exec( `INSERT INTO users (id, tenant_id, domain_id, email, password_hash, display_name, role, active) VALUES (?, ?, ?, ?, ?, ?, 'global_admin', 1)`, uuid.NewString(), tenantID, domainID, adminEmail, string(hash), "Global Admin", ); err != nil { return fmt.Errorf("creating admin user: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("committing bootstrap: %w", err) } slog.Info("bootstrap complete", "admin_email", adminEmail, "tenant", "default", "domain", hostname) return nil }