package admin import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/rose_cat707/luminary/internal/auth" "github.com/rose_cat707/luminary/internal/auth/loginlimit" "github.com/rose_cat707/luminary/internal/gateway" "github.com/rose_cat707/luminary/internal/middleware" "github.com/rose_cat707/luminary/internal/model" "github.com/rose_cat707/luminary/internal/monitor" "github.com/rose_cat707/luminary/internal/prediction" "github.com/rose_cat707/luminary/internal/settings" "github.com/rose_cat707/luminary/internal/storage/imagestore" "github.com/rose_cat707/luminary/internal/store/cache" sqlitestore "github.com/rose_cat707/luminary/internal/store/sqlite" ) type Handler struct { repo *sqlitestore.Repository db *sqlitestore.Store cache *cache.Store gateway *gateway.Service settings *settings.Runtime monitor *monitor.Service loginLimit *loginlimit.Limiter predictions *prediction.Service images *imagestore.Store publicURL func() string } func New(repo *sqlitestore.Repository, db *sqlitestore.Store, c *cache.Store, gw *gateway.Service, rt *settings.Runtime, mon *monitor.Service, pred *prediction.Service, imgs *imagestore.Store, publicURL func() string) *Handler { return &Handler{ repo: repo, db: db, cache: c, gateway: gw, settings: rt, monitor: mon, loginLimit: loginlimit.New(rt.LoginMaxAttempts(), rt.LoginLockout()), predictions: pred, images: imgs, publicURL: publicURL, } } func (h *Handler) publicBaseURL() string { if h.publicURL != nil { return h.publicURL() } return "http://localhost:8293" } func (h *Handler) Register(r *gin.RouterGroup) { r.POST("/login", h.Login) r.GET("/auth/status", h.AuthStatus) authGroup := r.Group("", middleware.AdminAuth(h.validateSession)) authGroup.POST("/logout", h.Logout) authGroup.PUT("/password", h.ChangePassword) authGroup.GET("/endpoint-meta", h.ListEndpointMeta) authGroup.GET("/providers", h.ListProviders) authGroup.POST("/providers", h.CreateProvider) authGroup.GET("/providers/:id", h.GetProvider) authGroup.PUT("/providers/:id", h.UpdateProvider) authGroup.DELETE("/providers/:id", h.DeleteProvider) authGroup.POST("/providers/:id/sync-models", h.SyncModels) authGroup.GET("/providers/:id/keys", h.ListProviderKeys) authGroup.POST("/providers/:id/keys", h.CreateProviderKey) authGroup.PUT("/provider-keys/:keyId", h.UpdateProviderKey) authGroup.DELETE("/provider-keys/:keyId", h.DeleteProviderKey) authGroup.GET("/providers/:id/models", h.ListModels) authGroup.POST("/providers/:id/models", h.CreateModel) authGroup.PUT("/providers/:id/models/:modelId", h.UpdateModel) authGroup.DELETE("/providers/:id/models/:modelId", h.DeleteModel) authGroup.PUT("/models/:modelId", h.UpdateModel) authGroup.DELETE("/models/:modelId", h.DeleteModel) authGroup.GET("/ingress-keys", h.ListIngressKeys) authGroup.POST("/ingress-keys", h.CreateIngressKey) authGroup.GET("/ingress-keys/:id", h.GetIngressKey) authGroup.PUT("/ingress-keys/:id", h.UpdateIngressKey) authGroup.DELETE("/ingress-keys/:id", h.DeleteIngressKey) authGroup.GET("/ingress-keys/:id/usage", h.IngressUsage) authGroup.GET("/ingress-keys/:id/routing-rules", h.ListRoutingRules) authGroup.POST("/ingress-keys/:id/routing-rules", h.CreateRoutingRule) authGroup.PUT("/routing-rules/:ruleId", h.UpdateRoutingRule) authGroup.DELETE("/routing-rules/:ruleId", h.DeleteRoutingRule) authGroup.GET("/workflows/param-meta", h.GetWorkflowParamMeta) authGroup.POST("/workflows/analyze", h.AnalyzeWorkflow) authGroup.GET("/predictions/:id", h.GetPrediction) authGroup.GET("/images/:id", h.ServeAdminImage) authGroup.GET("/request-logs", h.ListRequestLogs) authGroup.GET("/request-logs/:id", h.GetRequestLog) authGroup.GET("/pricing/catalog", h.PricingCatalog) authGroup.GET("/ip-rules", h.ListIPRules) authGroup.POST("/ip-rules", h.CreateIPRule) authGroup.PUT("/ip-rules/:id", h.UpdateIPRule) authGroup.DELETE("/ip-rules/:id", h.DeleteIPRule) authGroup.GET("/settings", h.GetSettings) authGroup.PUT("/settings", h.UpdateSettings) authGroup.GET("/dashboard/overview", h.DashboardOverview) authGroup.GET("/dashboard/providers", h.DashboardProviders) authGroup.GET("/dashboard/ingress-usage", h.DashboardIngressUsage) authGroup.GET("/dashboard/egress-usage", h.DashboardEgressUsage) authGroup.GET("/dashboard/provider-model-usage", h.DashboardProviderModelUsage) authGroup.GET("/dashboard/client-ip-usage", h.DashboardClientIPUsage) authGroup.GET("/dashboard/request-trend", h.DashboardRequestTrend) } func (h *Handler) validateSession(token string) (bool, time.Time) { hash := auth.HashToken(token) var session model.AdminSession if err := h.db.DB().Where("token_hash = ?", hash).First(&session).Error; err != nil { return false, time.Time{} } return session.ExpiresAt.After(time.Now()), session.ExpiresAt } func (h *Handler) Login(c *gin.Context) { ip := middleware.ClientIP(c) if locked, retryAfter := h.loginLimit.Check(ip); locked { sec := int(retryAfter.Seconds()) + 1 c.Header("Retry-After", strconv.Itoa(sec)) h.recordLoginLog(ip, "", "error", "登录尝试次数过多", "", fmt.Sprintf(`{"retry_after_sec":%d}`, sec)) c.JSON(http.StatusTooManyRequests, gin.H{ "error": "登录尝试次数过多,请稍后再试", "retry_after_sec": sec, }) return } var req struct { Username string `json:"username"` Password string `json:"password"` } if err := c.ShouldBindJSON(&req); err != nil { h.recordLoginLog(ip, "", "error", "invalid request", "", "") c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } reqBody := loginRequestBody(req.Username) var user model.AdminUser if err := h.db.DB().Where("username = ?", req.Username).First(&user).Error; err != nil { h.loginLimit.RecordFailure(ip) h.recordLoginLog(ip, req.Username, "error", "invalid credentials", reqBody, "") c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } if !auth.CheckPassword(user.PasswordHash, req.Password) { h.loginLimit.RecordFailure(ip) h.recordLoginLog(ip, req.Username, "error", "invalid credentials", reqBody, "") c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } h.loginLimit.Reset(ip) token, err := generateSessionToken() if err != nil { h.recordLoginLog(ip, req.Username, "error", "session error", reqBody, "") c.JSON(http.StatusInternalServerError, gin.H{"error": "session error"}) return } session := model.AdminSession{ TokenHash: auth.HashToken(token), ExpiresAt: time.Now().Add(h.settings.SessionTTL()), } if err := h.db.DB().Create(&session).Error; err != nil { h.recordLoginLog(ip, req.Username, "error", "session error", reqBody, "") c.JSON(http.StatusInternalServerError, gin.H{"error": "session error"}) return } respBody := fmt.Sprintf(`{"expires_at":%q}`, session.ExpiresAt.Format(time.RFC3339)) h.recordLoginLog(ip, req.Username, "success", "", reqBody, respBody) c.SetSameSite(http.SameSiteStrictMode) c.SetCookie(middleware.SessionCookieName(), token, int(h.settings.SessionTTL().Seconds()), "/", "", false, true) c.JSON(http.StatusOK, gin.H{"token": token, "expires_at": session.ExpiresAt}) } func loginRequestBody(username string) string { b, _ := json.Marshal(map[string]string{"username": username}) return string(b) } func (h *Handler) recordLoginLog(ip, username, status, errMsg, requestBody, responseBody string) { h.repo.RecordRequestLog(model.UsageRecord{ ClientIP: ip, Endpoint: model.EndpointAdminLogin, Model: username, Status: status, ErrorMsg: errMsg, RequestBody: requestBody, ResponseBody: responseBody, }) } func (h *Handler) Logout(c *gin.Context) { token, _ := c.Get("session_token") if t, ok := token.(string); ok { h.db.DB().Where("token_hash = ?", auth.HashToken(t)).Delete(&model.AdminSession{}) } c.SetCookie(middleware.SessionCookieName(), "", -1, "/", "", false, true) c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *Handler) AuthStatus(c *gin.Context) { token := c.GetHeader("Authorization") if len(token) > 7 { token = token[7:] } if token == "" { if cookie, err := c.Cookie(middleware.SessionCookieName()); err == nil { token = cookie } } valid := false if token != "" { valid, _ = h.validateSession(token) } c.JSON(http.StatusOK, gin.H{"authenticated": valid}) } func (h *Handler) ChangePassword(c *gin.Context) { var req struct { OldPassword string `json:"old_password"` NewPassword string `json:"new_password"` } if err := c.ShouldBindJSON(&req); err != nil || req.NewPassword == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } var user model.AdminUser if err := h.db.DB().First(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "user not found"}) return } if !auth.CheckPassword(user.PasswordHash, req.OldPassword) { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid old password"}) return } hash, err := auth.HashPassword(req.NewPassword) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "hash error"}) return } user.PasswordHash = hash h.db.DB().Save(&user) c.JSON(http.StatusOK, gin.H{"ok": true}) } func generateSessionToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }