1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/bootstrap/admin_account.go
chan 31d107ff2e feat(user): support fixed UUID registration and enhance bulk import results
- Added support for fixed UUIDs during bulk registration (Search-first + ExternalID mapping)
- Implemented idempotency and visibility restoration for soft-deleted users
- Enhanced bulk upload UI to show 'New/Updated/Unchanged' status and modified fields
- Added logic to reclaim identifiers (login_id) from colliding records
- Added frontend E2E and backend unit tests for UUID integrity and conflict handling
- Fixed i18n, formatting, and mock tests to satisfy code-check
- Applied 'go fix' for 'omitzero' tags and general Go standards
2026-06-01 15:34:08 +09:00

204 lines
6.0 KiB
Go

package bootstrap
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"context"
"errors"
"fmt"
"net/mail"
"strings"
"time"
"gorm.io/gorm"
)
type SuperAdminIdentityAdmin interface {
FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error)
CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error)
UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error
}
type SuperAdminStore interface {
FindUserByEmail(ctx context.Context, email string) (*domain.User, error)
CreateUser(ctx context.Context, user *domain.User) error
UpdateUserSuperAdmin(ctx context.Context, userID string, name string) (*domain.User, error)
EnqueueSuperAdminRelation(ctx context.Context, userID string) error
}
type EnsureSuperAdminOptions struct {
Email string
Password string
Name string
Source string
UpdatePassword bool
}
type EnsureSuperAdminResult struct {
Email string
IdentityID string
LocalUserID string
IdentityCreated bool
PasswordUpdated bool
LocalUserCreated bool
LocalUserUpdated bool
KetoRelationQueued bool
}
func EnsureSuperAdmin(ctx context.Context, identityAdmin SuperAdminIdentityAdmin, store SuperAdminStore, opts EnsureSuperAdminOptions) (EnsureSuperAdminResult, error) {
email := strings.ToLower(strings.TrimSpace(opts.Email))
name := strings.TrimSpace(opts.Name)
if name == "" {
name = "System Admin"
}
source := strings.TrimSpace(opts.Source)
if source == "" {
source = "admin_cli"
}
result := EnsureSuperAdminResult{Email: email}
if _, err := mail.ParseAddress(email); err != nil {
return result, fmt.Errorf("invalid admin email: %w", err)
}
if identityAdmin == nil {
return result, errors.New("identity admin is required")
}
if store == nil {
return result, errors.New("super admin store is required")
}
identityID, err := identityAdmin.FindIdentityIDByIdentifier(ctx, email)
if err != nil {
return result, fmt.Errorf("find admin identity: %w", err)
}
if identityID == "" {
if strings.TrimSpace(opts.Password) == "" {
return result, errors.New("admin password is required to create identity")
}
identityID, err = identityAdmin.CreateUser(ctx, buildSuperAdminBrokerUser(email, name), opts.Password)
if err != nil {
return result, fmt.Errorf("create admin identity: %w", err)
}
result.IdentityCreated = true
} else if opts.UpdatePassword {
if strings.TrimSpace(opts.Password) == "" {
return result, errors.New("admin password is required to update identity password")
}
if err := identityAdmin.UpdateIdentityPassword(ctx, identityID, opts.Password); err != nil {
return result, fmt.Errorf("update admin identity password: %w", err)
}
result.PasswordUpdated = true
}
result.IdentityID = identityID
user, err := store.FindUserByEmail(ctx, email)
if err != nil {
return result, fmt.Errorf("find local admin user: %w", err)
}
if user == nil {
if identityID == "" {
return result, errors.New("identity id is required to create local admin user")
}
user = &domain.User{
ID: identityID,
Email: email,
Name: name,
Role: domain.RoleSuperAdmin,
Status: domain.UserStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Metadata: domain.JSONMap{
"source": source,
},
}
if err := store.CreateUser(ctx, user); err != nil {
return result, fmt.Errorf("create local admin user: %w", err)
}
result.LocalUserCreated = true
} else if domain.NormalizeRole(user.Role) != domain.RoleSuperAdmin || user.Status != domain.UserStatusActive || (name != "" && user.Name != name) {
user, err = store.UpdateUserSuperAdmin(ctx, user.ID, name)
if err != nil {
return result, fmt.Errorf("update local admin user: %w", err)
}
result.LocalUserUpdated = true
}
result.LocalUserID = user.ID
if err := store.EnqueueSuperAdminRelation(ctx, user.ID); err != nil {
return result, fmt.Errorf("enqueue super admin keto relation: %w", err)
}
result.KetoRelationQueued = true
return result, nil
}
func buildSuperAdminBrokerUser(email, name string) *domain.BrokerUser {
return &domain.BrokerUser{
Email: email,
Name: name,
PhoneNumber: "",
Attributes: map[string]any{
"department": "Admin",
"affiliationType": "internal",
"grade": "",
"role": domain.RoleSuperAdmin,
},
}
}
type gormSuperAdminStore struct {
db *gorm.DB
outbox repository.KetoOutboxRepository
}
func NewGormSuperAdminStore(db *gorm.DB, outbox repository.KetoOutboxRepository) SuperAdminStore {
return &gormSuperAdminStore{db: db, outbox: outbox}
}
func (s *gormSuperAdminStore) FindUserByEmail(ctx context.Context, email string) (*domain.User, error) {
var user domain.User
if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (s *gormSuperAdminStore) CreateUser(ctx context.Context, user *domain.User) error {
return s.db.WithContext(ctx).Create(user).Error
}
func (s *gormSuperAdminStore) UpdateUserSuperAdmin(ctx context.Context, userID string, name string) (*domain.User, error) {
updates := map[string]any{
"role": domain.RoleSuperAdmin,
"status": domain.UserStatusActive,
"updated_at": time.Now(),
}
if strings.TrimSpace(name) != "" {
updates["name"] = strings.TrimSpace(name)
}
if err := s.db.WithContext(ctx).Model(&domain.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
return nil, err
}
var user domain.User
if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (s *gormSuperAdminStore) EnqueueSuperAdminRelation(ctx context.Context, userID string) error {
if s.outbox == nil {
return nil
}
return s.outbox.Create(ctx, &domain.KetoOutbox{
Namespace: "System",
Object: "global",
Relation: "super_admins",
Subject: "User:" + userID,
Action: domain.KetoOutboxActionCreate,
})
}