57 lines
1.8 KiB
Go
57 lines
1.8 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"galaxy/backend/internal/config"
|
|
|
|
"go.uber.org/zap"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Bootstrap inserts the seed admin row when the env-driven
|
|
// `BACKEND_ADMIN_BOOTSTRAP_USER` / `BACKEND_ADMIN_BOOTSTRAP_PASSWORD`
|
|
// values are supplied and no row with that username exists yet. The
|
|
// insert is idempotent across restarts so operators can leave the env
|
|
// vars set after the first deploy without re-creating the row on
|
|
// every boot.
|
|
//
|
|
// Bootstrap runs *before* `Cache.Warm` so the warm read picks up the
|
|
// seed row. Errors are returned to the caller; the boot path in
|
|
// `cmd/backend/main.go` aborts startup if Bootstrap fails (a missing
|
|
// admin would lock the surface out anyway, so failing fast is the
|
|
// safer default).
|
|
//
|
|
// When both env vars are empty the function logs "skipped" and
|
|
// returns nil. `config.Validate()` already enforces that the username
|
|
// and password are set together, so by the time Bootstrap runs the
|
|
// remaining "user set without password" combination is impossible.
|
|
func Bootstrap(ctx context.Context, store *Store, cfg config.AdminBootstrapConfig, logger *zap.Logger) error {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
logger = logger.Named("admin.bootstrap")
|
|
|
|
if cfg.User == "" {
|
|
logger.Info("skipped (no env vars)")
|
|
return nil
|
|
}
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.Password), bootstrapBcryptCost)
|
|
if err != nil {
|
|
return fmt.Errorf("admin bootstrap: hash password: %w", err)
|
|
}
|
|
|
|
inserted, err := store.BootstrapInsert(ctx, cfg.User, hash)
|
|
if err != nil {
|
|
return fmt.Errorf("admin bootstrap: %w", err)
|
|
}
|
|
if inserted {
|
|
logger.Info("inserted seed admin", zap.String("admin_username", cfg.User))
|
|
} else {
|
|
logger.Info("skipped (admin exists)", zap.String("admin_username", cfg.User))
|
|
}
|
|
return nil
|
|
}
|