package web import ( "encoding/json" "errors" "net/http" "strings" "ymhut-box/server/unified-management/internal/auth" "ymhut-box/server/unified-management/internal/config" "ymhut-box/server/unified-management/internal/db" "ymhut-box/server/unified-management/internal/feedback" "ymhut-box/server/unified-management/internal/legacy" "ymhut-box/server/unified-management/internal/notices" "ymhut-box/server/unified-management/internal/releases" "ymhut-box/server/unified-management/internal/sources" "ymhut-box/server/unified-management/internal/synclegacy" ) type router struct { cfg *config.Config store *db.Store auth *auth.Service feedback *feedback.Service releases *releases.Service sources *sources.Service legacy *legacy.Service notices *notices.Service syncer *synclegacy.Service } func NewRouter(cfg *config.Config, store *db.Store, authService *auth.Service, feedbackService *feedback.Service, releaseService *releases.Service, sourceService *sources.Service, legacyService *legacy.Service, optional ...any) http.Handler { r := &router{ cfg: cfg, store: store, auth: authService, feedback: feedbackService, releases: releaseService, sources: sourceService, legacy: legacyService, } for _, item := range optional { switch typed := item.(type) { case *notices.Service: r.notices = typed case *synclegacy.Service: r.syncer = typed } } return withSecurity(r) } func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { path := cleanPath(req.URL.Path) switch { case path == "/" && req.Method == http.MethodPost: r.handleFeedbackSubmit(w, req) case path == "/" && req.URL.Query().Get("api") == "status": r.handleFeedbackStatus(w, req) case isPortalRoute(path): r.servePortal(w, req) case path == "/api/auth/bootstrap" || path == "/api/admin/auth/bootstrap": r.handleAuthBootstrap(w, req) case path == "/api/auth/captcha" || path == "/api/admin/auth/captcha": r.handleCaptcha(w, req) case path == "/api/auth/login" || path == "/api/admin/auth/login": r.handleLogin(w, req) case path == "/api/auth/logout" || path == "/api/admin/auth/logout": r.auth.Require(http.HandlerFunc(r.handleLogout)).ServeHTTP(w, req) case path == "/api/admin/auth/password": r.auth.Require(http.HandlerFunc(r.handleChangePassword)).ServeHTTP(w, req) case path == "/api/client/bootstrap": r.handleClientBootstrap(w, req) case path == "/api/client/releases" || path == "/api/releases" || path == "/api/update-info": writeJSON(w, http.StatusOK, r.releases.Manifest(req)) case path == "/api/client/sources": r.handleClientSources(w, req) case path == "/api/client/endpoints": r.handleClientEndpoints(w, req) case path == "/api/client/notices" || strings.HasPrefix(path, "/api/client/notices/"): r.handleClientNotices(w, req) case path == "/api/client/endpoint-calls" || path == "/api/client/source-calls": r.handleSourceCall(w, req) case path == "/update-info.json" || path == "/update-info": writeJSON(w, http.StatusOK, r.releases.LegacyUpdateInfo(req)) case path == "/tool-status.json" || path == "/tool-status": writeJSON(w, http.StatusOK, r.releases.StaticJSON("tool-status.json")) case path == "/modules.json" || path == "/modules" || path == "/api/modules": writeJSON(w, http.StatusOK, r.releases.StaticJSON("modules.json")) case path == "/media-types.json" || path == "/media-types": r.handleLegacyMediaTypes(w, req) case strings.HasPrefix(path, "/downloads/"): r.handleDownload(w, req) case strings.HasPrefix(path, "/admin/assets/"): serveStaticAsset(w, req, r.cfg.AdminWebDir, "admin/dist", strings.TrimPrefix(path, "/admin/")) case strings.HasPrefix(path, "/assets/"): serveStaticAsset(w, req, r.cfg.PortalWebDir, "portal/dist", strings.TrimPrefix(path, "/")) case strings.HasPrefix(path, "/api/admin/feedbacks"): r.auth.Require(http.HandlerFunc(r.handleAdminFeedbacks)).ServeHTTP(w, req) case strings.HasPrefix(path, "/api/admin/dashboard"): r.auth.Require(http.HandlerFunc(r.handleAdminDashboard)).ServeHTTP(w, req) case strings.HasPrefix(path, "/api/admin/sync"): r.auth.Require(http.HandlerFunc(r.handleAdminSync)).ServeHTTP(w, req) case strings.HasPrefix(path, "/api/admin/releases"): r.auth.Require(http.HandlerFunc(r.handleAdminReleases)).ServeHTTP(w, req) case strings.HasPrefix(path, "/api/admin/sources"): r.auth.Require(http.HandlerFunc(r.handleAdminSources)).ServeHTTP(w, req) case strings.HasPrefix(path, "/api/admin/endpoints"): r.auth.Require(http.HandlerFunc(r.handleAdminEndpoints)).ServeHTTP(w, req) case path == "/api/admin/events": r.auth.Require(http.HandlerFunc(r.handleAdminEvents)).ServeHTTP(w, req) case strings.HasPrefix(path, "/api/admin/legacy"): r.auth.Require(http.HandlerFunc(r.handleAdminLegacy)).ServeHTTP(w, req) case strings.HasPrefix(path, "/api/admin/database"): r.auth.Require(http.HandlerFunc(r.handleAdminDatabase)).ServeHTTP(w, req) case strings.HasPrefix(path, "/api/admin/system"): r.auth.Require(http.HandlerFunc(r.handleAdminSystem)).ServeHTTP(w, req) case path == "/admin" || path == "/admin/": http.Redirect(w, req, "/admin/dashboard", http.StatusFound) case path == "/admin/login" || strings.HasPrefix(path, "/admin/"): r.serveAdmin(w, req) default: http.NotFound(w, req) } } func (r *router) handleAuthBootstrap(w http.ResponseWriter, req *http.Request) { payload, err := r.auth.Bootstrap(req.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "BOOTSTRAP_FAILED", err) return } writeJSON(w, http.StatusOK, payload) } func (r *router) handleCaptcha(w http.ResponseWriter, req *http.Request) { captcha, err := r.auth.NewCaptcha() if err != nil { writeError(w, http.StatusInternalServerError, "CAPTCHA_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "captchaId": captcha.ID, "image": captcha.Image}) } func (r *router) handleLogin(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", errors.New("POST required")) return } var body struct { Username string `json:"username"` Password string `json:"password"` CaptchaID string `json:"captchaId"` Captcha string `json:"captcha"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err) return } if body.Username == "" { body.Username = "admin" } sessionID, csrf, ok, err := r.auth.Login(req.Context(), body.Username, body.Password, body.CaptchaID, body.Captcha, req.RemoteAddr) if err != nil { writeError(w, http.StatusInternalServerError, "LOGIN_FAILED", err) return } if !ok { writeError(w, http.StatusOK, "LOGIN_FAILED", errors.New("invalid password or captcha")) return } auth.SetSessionCookieForRequest(w, req, sessionID) _ = r.store.InsertAudit(db.AuditLog{Actor: body.Username, Type: "auth.login", Target: "admin", Message: "管理员登录", IP: req.RemoteAddr, UserAgent: req.UserAgent()}) writeJSON(w, http.StatusOK, map[string]any{"ok": true, "csrfToken": csrf, "user": map[string]any{"username": body.Username}}) } func (r *router) handleLogout(w http.ResponseWriter, req *http.Request) { r.auth.Logout(w, req) writeJSON(w, http.StatusOK, map[string]any{"ok": true}) } func (r *router) handleChangePassword(w http.ResponseWriter, req *http.Request) { var body struct { CurrentPassword string `json:"currentPassword"` NewPassword string `json:"newPassword"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err) return } warning, err := r.store.ChangeAdminPasswordWithWarning(req.Context(), "admin", body.CurrentPassword, body.NewPassword) if err != nil { writeError(w, http.StatusBadRequest, "PASSWORD_CHANGE_FAILED", err) return } _ = r.store.InsertAudit(db.AuditLog{Actor: "admin", Type: "auth.password_changed", Target: "admin", Message: "后台密码已修改", IP: req.RemoteAddr, UserAgent: req.UserAgent()}) payload := map[string]any{"ok": true, "isDefaultPassword": false} if warning != "" { payload["warning"] = warning } writeJSON(w, http.StatusOK, payload) }