first commit
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user