203 lines
8.1 KiB
Go
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)
|
|
}
|