fix: scale replicas in response, K8s metrics client, quota precheck, auth tests

- Add GetMetrics method to MetricsClient interface and implement cluster metrics API
- Add QuotaPrecheck service for validating resource quotas before deployment
- Add auth DTO with role/permission models and auth handler tests
- Add instance diagnostics: mounted NFS volumes, labels, annotations in pod diagnostics
- Update workspace handler with GetWorkspace endpoint and shared-user list
- Fix monitoring handler to use correct service method name
- Add tail_lines fallback in instance handler for snake_case query params
- Update nginx config for SSE log streaming support (no buffering)
- Add comprehensive test coverage: auth_service_test, auth_handler_test,
  auth_dto_test, metrics_client_test, quota_precheck_test
- Update error messages for quota validation and instance operations
- ModifyModal: fix YAML lineWidth:0, modified keys summary, delta-only submit
- InstanceCard: correctly disable scale-minus when replicas <= 0
- SidebarLayout: add hover transition for sidebar items
- Update todo.md and lessons.md with latest fixes
This commit is contained in:
Ivan087
2026-05-20 16:56:29 +08:00
parent 8f90cf0f0d
commit 33ddaf97db
59 changed files with 4805 additions and 457 deletions

View File

@ -3,8 +3,11 @@ package rest
import (
"context"
"encoding/json"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/gorilla/mux"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
@ -18,6 +21,74 @@ type AuthHandler struct {
authService *service.AuthService
}
const (
loginRateLimitWindow = time.Minute
loginRateLimitFailures = 5
)
var defaultLoginRateLimiter = newLoginRateLimiter(loginRateLimitWindow, loginRateLimitFailures)
type loginRateLimiter struct {
mu sync.Mutex
window time.Duration
limit int
failures map[string]loginFailureState
now func() time.Time
}
type loginFailureState struct {
count int
windowEnds time.Time
}
func newLoginRateLimiter(window time.Duration, limit int) *loginRateLimiter {
return &loginRateLimiter{
window: window,
limit: limit,
failures: make(map[string]loginFailureState),
now: time.Now,
}
}
func (l *loginRateLimiter) Allow(key string) bool {
if l == nil || key == "" {
return true
}
l.mu.Lock()
defer l.mu.Unlock()
state, ok := l.failures[key]
now := l.now()
if !ok || now.After(state.windowEnds) {
return true
}
return state.count < l.limit
}
func (l *loginRateLimiter) RecordFailure(key string) {
if l == nil || key == "" {
return
}
l.mu.Lock()
defer l.mu.Unlock()
now := l.now()
state, ok := l.failures[key]
if !ok || now.After(state.windowEnds) {
l.failures[key] = loginFailureState{count: 1, windowEnds: now.Add(l.window)}
return
}
state.count++
l.failures[key] = state
}
func (l *loginRateLimiter) Reset(key string) {
if l == nil || key == "" {
return
}
l.mu.Lock()
defer l.mu.Unlock()
delete(l.failures, key)
}
// NewAuthHandler 创建认证 Handler
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{
@ -41,6 +112,7 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
req.Normalize()
// 调用领域服务
user, err := h.authService.Register(r.Context(), req.Username, req.Password, req.Role, req.WorkspaceID, service.UserWorkspaceOptions{
@ -79,6 +151,7 @@ func (h *AuthHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
req.Normalize()
user, err := h.authService.UpdateUser(r.Context(), userID, req.Role, req.WorkspaceID, service.UserWorkspaceOptions{
Namespace: req.Namespace,
DefaultClusterID: req.DefaultClusterID,
@ -120,12 +193,21 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
return
}
rateLimitKey := loginRateLimitKey(r, req.Username)
if !defaultLoginRateLimiter.Allow(rateLimitKey) {
w.Header().Set("Retry-After", "60")
respondError(w, http.StatusTooManyRequests, "Too many login attempts", "too many login attempts; retry later")
return
}
// 调用领域服务
accessToken, refreshToken, user, err := h.authService.Login(r.Context(), req.Username, req.Password)
if err != nil {
respondError(w, http.StatusUnauthorized, "Login failed", err.Error())
defaultLoginRateLimiter.RecordFailure(rateLimitKey)
respondError(w, http.StatusUnauthorized, "Invalid username or password", "invalid username or password")
return
}
defaultLoginRateLimiter.Reset(rateLimitKey)
workspace, _ := h.authService.GetWorkspaceByID(r.Context(), user.WorkspaceID)
@ -151,6 +233,23 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, response)
}
func loginRateLimitKey(r *http.Request, username string) string {
client := strings.TrimSpace(r.Header.Get("X-Forwarded-For"))
if idx := strings.Index(client, ","); idx >= 0 {
client = strings.TrimSpace(client[:idx])
}
if client == "" {
client = strings.TrimSpace(r.Header.Get("X-Real-IP"))
}
if client == "" {
client = r.RemoteAddr
if host, _, err := net.SplitHostPort(client); err == nil {
client = host
}
}
return strings.ToLower(strings.TrimSpace(username)) + "|" + client
}
func (h *AuthHandler) convertUserResponse(ctx context.Context, user *entity.User) *dto.UserResponse {
workspace, _ := h.authService.GetWorkspaceByID(ctx, user.WorkspaceID)
return &dto.UserResponse{