feat(server/ops-api): add operations console backend
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>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user