refactor: simplify setup flow — eliminate redundant DB calls and login round-trips

- Add AdminExists() to UserRepository (EXISTS query, not full table scan)
- SetupInitialAdmin returns tokens directly (skip separate Login call)
- Add SetupRequest DTO to auth_dto.go (replace inline struct)
- Extract defaultEmail() helper (removes duplicated email logic)
- AuthPage uses setup tokens directly (skip redundant apiLogin call)
- Use customAxiosInstance for auth API calls (consistent with codebase)
This commit is contained in:
Ivan087
2026-05-21 14:22:52 +08:00
parent 7d297a2b1a
commit e73b3147ed
8 changed files with 61 additions and 45 deletions

View File

@ -50,6 +50,13 @@ type LoginRequest struct {
Password string `json:"password" binding:"required"`
}
// SetupRequest 初始管理员注册请求
type SetupRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email,omitempty"`
}
// RefreshTokenRequest 刷新 Token 请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refreshToken" binding:"required"`

View File

@ -265,11 +265,7 @@ func (h *AuthHandler) AuthStatus(w http.ResponseWriter, r *http.Request) {
// Setup creates the first admin user. Only works when no admin exists.
func (h *AuthHandler) Setup(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
}
var req dto.SetupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
@ -279,19 +275,12 @@ func (h *AuthHandler) Setup(w http.ResponseWriter, r *http.Request) {
return
}
_, err := h.authService.SetupInitialAdmin(r.Context(), req.Username, req.Password, req.Email)
_, accessToken, refreshToken, err := h.authService.SetupInitialAdmin(r.Context(), req.Username, req.Password, req.Email)
if err != nil {
respondServiceError(w, err, "Failed to create initial admin")
return
}
// Generate access token immediately
accessToken, refreshToken, _, err := h.authService.Login(r.Context(), req.Username, req.Password)
if err != nil {
respondServiceError(w, err, "Admin created but login failed")
return
}
respondJSON(w, http.StatusCreated, map[string]string{
"accessToken": accessToken,
"refreshToken": refreshToken,

View File

@ -85,6 +85,17 @@ func (r *UserRepositoryMock) Delete(ctx context.Context, id string) error {
return nil
}
func (r *UserRepositoryMock) AdminExists(ctx context.Context) (bool, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, u := range r.users {
if u.Role == "admin" {
return true, nil
}
}
return false, nil
}
func (r *UserRepositoryMock) List(ctx context.Context) ([]*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()

View File

@ -120,6 +120,12 @@ func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*e
}
// Update 更新用户
func (r *UserRepository) AdminExists(ctx context.Context) (bool, error) {
var exists bool
err := r.db.conn.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM users WHERE role = 'admin')`).Scan(&exists)
return exists, err
}
func (r *UserRepository) Update(ctx context.Context, user *entity.User) error {
user.UpdatedAt = time.Now()

View File

@ -24,4 +24,7 @@ type UserRepository interface {
// List 列出所有用户
List(ctx context.Context) ([]*entity.User, error)
// AdminExists checks whether any admin user exists (lightweight EXISTS query)
AdminExists(ctx context.Context) (bool, error)
}

View File

@ -80,53 +80,53 @@ 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
func defaultEmail(username string) string {
return username + "@local.ocdp"
}
// 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) {
// IsAdminExists checks whether any admin user already exists in the database.
func (s *AuthService) IsAdminExists(ctx context.Context) (bool, error) {
return s.userRepo.AdminExists(ctx)
}
// SetupInitialAdmin creates the first admin user and returns access + refresh tokens.
// Fails if an admin already exists.
func (s *AuthService) SetupInitialAdmin(ctx context.Context, username, password, email string) (*entity.User, string, string, error) {
hasAdmin, err := s.IsAdminExists(ctx)
if err != nil {
return nil, err
return nil, "", "", err
}
if hasAdmin {
return nil, entity.ErrForbidden
return nil, "", "", entity.ErrForbidden
}
// Hash password
passwordHash, err := s.passwordHasher.Hash(password)
if err != nil {
return nil, err
return nil, "", "", err
}
if email == "" {
email = username + "@local.ocdp"
email = defaultEmail(username)
}
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
return nil, "", "", err
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
return nil, "", "", err
}
return user, nil
// Generate tokens directly — avoid a separate login round-trip
accessToken, refreshToken, err := s.tokenGenerator.Generate(user.ID, user.Username, user.Role, user.WorkspaceID)
if err != nil {
return nil, "", "", err
}
return user, accessToken, refreshToken, nil
}
func (s *AuthService) Register(ctx context.Context, username, password, role, workspaceID string, opts UserWorkspaceOptions, isActive, mustChangePassword *bool) (*entity.User, error) {
@ -158,7 +158,7 @@ func (s *AuthService) Register(ctx context.Context, username, password, role, wo
}
// 默认生成占位邮箱,避免数据库约束失败
email := username + "@local.ocdp"
email := defaultEmail(username)
// 创建用户
user := entity.NewUser(username, passwordHash, email)