c091ad7aed
Stand up an internal ops-api service with operator account, auth, and audit log subsystems backed by a dedicated migrations_ops migration set. Wire Makefile targets and the migrate Dockerfile stage so the new schema ships alongside the existing tenant + monitoring databases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
1.9 KiB
Go
70 lines
1.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/ops/domain"
|
|
"github.com/geo-platform/tenant-api/internal/ops/repository"
|
|
)
|
|
|
|
type DefaultAdminSeed struct {
|
|
Username string
|
|
Password string
|
|
DisplayName string
|
|
Email string
|
|
}
|
|
|
|
// SeedDefaultAdmin inserts a single admin account when the operator_accounts
|
|
// table is empty. It is safe to call on every startup — a non-empty table is
|
|
// a no-op. Returns an error only when the table is empty AND the seed config
|
|
// is incomplete (so a fresh deployment cannot silently boot without any
|
|
// way to log in).
|
|
func SeedDefaultAdmin(ctx context.Context, accounts *repository.AccountRepository, seed DefaultAdminSeed, logger *zap.Logger) error {
|
|
count, err := accounts.Count(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("count operator accounts: %w", err)
|
|
}
|
|
if count > 0 {
|
|
return nil
|
|
}
|
|
|
|
username := strings.TrimSpace(seed.Username)
|
|
password := seed.Password
|
|
if username == "" || password == "" {
|
|
return errors.New("operator_accounts is empty but default_admin.username/password are not configured (set OPS_DEFAULT_ADMIN_USERNAME / OPS_DEFAULT_ADMIN_PASSWORD)")
|
|
}
|
|
displayName := strings.TrimSpace(seed.DisplayName)
|
|
if displayName == "" {
|
|
displayName = username
|
|
}
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return fmt.Errorf("hash default admin password: %w", err)
|
|
}
|
|
|
|
var emailPtr *string
|
|
if email := strings.TrimSpace(seed.Email); email != "" {
|
|
emailPtr = &email
|
|
}
|
|
|
|
_, err = accounts.Create(ctx, repository.CreateAccountInput{
|
|
Username: username,
|
|
Email: emailPtr,
|
|
PasswordHash: string(hash),
|
|
DisplayName: displayName,
|
|
Role: domain.RoleAdmin,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("create default admin: %w", err)
|
|
}
|
|
logger.Sugar().Infof("ops default admin seeded: username=%s", username)
|
|
return nil
|
|
}
|