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

@ -250,6 +250,54 @@ func loginRateLimitKey(r *http.Request, username string) string {
return strings.ToLower(strings.TrimSpace(username)) + "|" + client
}
// AuthStatus returns whether the system needs initial setup (no admin exists).
func (h *AuthHandler) AuthStatus(w http.ResponseWriter, r *http.Request) {
hasAdmin, err := h.authService.IsAdminExists(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to check status", err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]any{
"needsSetup": !hasAdmin,
"hasUsers": hasAdmin,
})
}
// 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"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
if strings.TrimSpace(req.Username) == "" || strings.TrimSpace(req.Password) == "" {
respondError(w, http.StatusBadRequest, "Missing fields", "username and password are required")
return
}
_, 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,
})
}
func (h *AuthHandler) convertUserResponse(ctx context.Context, user *entity.User) *dto.UserResponse {
workspace, _ := h.authService.GetWorkspaceByID(ctx, user.WorkspaceID)
return &dto.UserResponse{