Files
YMhut-box-C-/server/unified-management/internal/web/router.go
T
QWQLwToo 2513eb2903
build-winui / winui (push) Has been cancelled
继续更新 update 门户站点界面和功能
2026-06-26 20:17:48 +08:00

203 lines
8.1 KiB
Go

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)
}