feat: first-time setup flow — no .env required for deployment

- Add GET /auth/status endpoint (returns needsSetup when no admin exists)
- Add POST /auth/setup endpoint (public first-admin registration)
- Add IsAdminExists + SetupInitialAdmin methods to AuthService
- Frontend: detect needsSetup on load, show setup page with admin registration
- Frontend: fall back to login page when setup is already complete
- Docker compose: env_file already optional (required: false), no changes needed
- Bootstrap: auto-detect BOOTSTRAP_CLUSTERS without separate enable flag
This commit is contained in:
Ivan087
2026-05-21 13:49:36 +08:00
parent 0144e9cab7
commit 0094519f52
5 changed files with 269 additions and 28 deletions

View File

@ -80,6 +80,55 @@ type UserWorkspaceOptions struct {
QuotaGPUMem string
}
// IsAdminExists checks whether any admin user already exists in the database.
func (s *AuthService) IsAdminExists(ctx context.Context) (bool, error) {
users, err := s.userRepo.List(ctx)
if err != nil {
return false, err
}
for _, u := range users {
if u.Role == authz.RoleAdmin {
return true, nil
}
}
return false, nil
}
// SetupInitialAdmin creates the first admin user. Fails if an admin already exists.
func (s *AuthService) SetupInitialAdmin(ctx context.Context, username, password, email string) (*entity.User, error) {
hasAdmin, err := s.IsAdminExists(ctx)
if err != nil {
return nil, err
}
if hasAdmin {
return nil, entity.ErrForbidden
}
// Hash password
passwordHash, err := s.passwordHasher.Hash(password)
if err != nil {
return nil, err
}
if email == "" {
email = username + "@local.ocdp"
}
user := entity.NewUser(username, passwordHash, email)
user.ID = uuid.New().String()
user.Role = authz.RoleAdmin
user.WorkspaceID = entity.DefaultWorkspaceID
user.IsActive = true
if err := user.Validate(); err != nil {
return nil, err
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *AuthService) Register(ctx context.Context, username, password, role, workspaceID string, opts UserWorkspaceOptions, isActive, mustChangePassword *bool) (*entity.User, error) {
principal, err := authz.RequirePrincipal(ctx)
if err != nil {