97 lines
3.2 KiB
Go
97 lines
3.2 KiB
Go
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
|
||
|
|
}
|