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:
@ -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{
|
||||
|
||||
Reference in New Issue
Block a user