mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
feat: merge conflict resolution, site management, UI fixes
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
// Package handler — API 配置 HTTP Handler。
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
// ApiConfigHandler API 配置 HTTP 处理。
|
||||
type ApiConfigHandler struct {
|
||||
svc *service.Container
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// NewApiConfigHandler 创建 API 配置处理器。
|
||||
func NewApiConfigHandler(svc *service.Container, log *zap.Logger) *ApiConfigHandler {
|
||||
return &ApiConfigHandler{svc: svc, log: log}
|
||||
}
|
||||
|
||||
// ListApiConfigs 获取所有 API 配置。
|
||||
// GET /api/api-config
|
||||
func (h *ApiConfigHandler) ListApiConfigs(c *gin.Context) {
|
||||
configs, err := h.svc.ApiConfig.List(c.Request.Context())
|
||||
if err != nil {
|
||||
h.log.Error("list api configs failed", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// 遮蔽 API Key
|
||||
for i := range configs {
|
||||
if configs[i].APIKey != "" {
|
||||
configs[i].APIKey = h.svc.ApiConfig.MaskAPIKey(configs[i].APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": configs})
|
||||
}
|
||||
|
||||
// ListProviders 获取预定义的提供者列表。
|
||||
// GET /api/api-config/providers/list
|
||||
func (h *ApiConfigHandler) ListProviders(c *gin.Context) {
|
||||
providers := h.svc.ApiConfig.GetProviders()
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": providers})
|
||||
}
|
||||
|
||||
// GetApiConfig 获取指定提供者的配置。
|
||||
// GET /api/api-config/:provider
|
||||
func (h *ApiConfigHandler) GetApiConfig(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := h.svc.ApiConfig.GetByProvider(c.Request.Context(), provider)
|
||||
if err != nil {
|
||||
if err == service.ErrApiConfigNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 40401, "message": "api config not found", "data": nil})
|
||||
return
|
||||
}
|
||||
h.log.Error("get api config failed", zap.Error(err), zap.String("provider", provider))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// 遮蔽 API Key
|
||||
if cfg.APIKey != "" {
|
||||
cfg.APIKey = h.svc.ApiConfig.MaskAPIKey(cfg.APIKey)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": cfg})
|
||||
}
|
||||
|
||||
// GetEffectiveConfig 获取生效的配置(数据库配置优先于配置文件)。
|
||||
// GET /api/api-config/:provider/effective
|
||||
func (h *ApiConfigHandler) GetEffectiveConfig(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := h.svc.ApiConfig.GetEffectiveConfig(c.Request.Context(), provider)
|
||||
if err != nil {
|
||||
if err == service.ErrApiConfigNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 40401, "message": "api config not found", "data": nil})
|
||||
return
|
||||
}
|
||||
h.log.Error("get effective config failed", zap.Error(err), zap.String("provider", provider))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// 遮蔽 API Key
|
||||
if cfg.APIKey != "" {
|
||||
cfg.APIKey = h.svc.ApiConfig.MaskAPIKey(cfg.APIKey)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": cfg})
|
||||
}
|
||||
|
||||
// UpsertApiConfig 创建或更新 API 配置。
|
||||
// POST /api/api-config/:provider
|
||||
func (h *ApiConfigHandler) UpsertApiConfig(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
APIKey string `json:"api_key"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Extra string `json:"extra"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "invalid request", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := h.svc.ApiConfig.Upsert(c.Request.Context(), provider, req.APIKey, req.BaseURL, req.Extra, req.Enabled)
|
||||
if err != nil {
|
||||
if err == service.ErrInvalidProvider {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "invalid provider", "data": nil})
|
||||
return
|
||||
}
|
||||
h.log.Error("upsert api config failed", zap.Error(err), zap.String("provider", provider))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回遮蔽后的配置
|
||||
cfg.APIKey = h.svc.ApiConfig.MaskAPIKey(cfg.APIKey)
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": cfg})
|
||||
}
|
||||
|
||||
// DeleteApiConfig 删除 API 配置。
|
||||
// DELETE /api/api-config/:provider
|
||||
func (h *ApiConfigHandler) DeleteApiConfig(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.ApiConfig.Delete(c.Request.Context(), provider); err != nil {
|
||||
h.log.Error("delete api config failed", zap.Error(err), zap.String("provider", provider))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": nil})
|
||||
}
|
||||
|
||||
// TestApiConfig 测试 API 连接。
|
||||
// POST /api/api-config/:provider/test
|
||||
func (h *ApiConfigHandler) TestApiConfig(c *gin.Context) {
|
||||
provider := c.Param("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "provider required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.svc.ApiConfig.TestConnection(c.Request.Context(), provider)
|
||||
if err != nil {
|
||||
h.log.Debug("test api config failed", zap.Error(err), zap.String("provider", provider))
|
||||
// 不返回错误,只返回测试结果
|
||||
}
|
||||
|
||||
// 更新测试结果
|
||||
_ = h.svc.ApiConfig.UpdateTestResult(c.Request.Context(), provider, result)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": gin.H{
|
||||
"result": result,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -28,20 +28,24 @@ func loginHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
u, token, err := svc.Auth.Login(c.Request.Context(), req.Username, req.Password)
|
||||
resp, err := svc.Auth.Login(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrInvalidCredentials) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrUserInactive) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "user account is inactive"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"user": u,
|
||||
"user": resp.User,
|
||||
"tokens": resp.Tokens,
|
||||
})
|
||||
svc.Audit.Record(c.Request.Context(), u.ID, "auth.login", u.Username, c.ClientIP(), "")
|
||||
svc.Audit.Record(c.Request.Context(), resp.User.ID, "auth.login", resp.User.Username, c.ClientIP(), "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +56,7 @@ func registerHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
u, err := svc.Auth.Register(c.Request.Context(), req.Username, req.Password)
|
||||
u, tokens, err := svc.Auth.Register(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrUsernameTaken) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "username taken"})
|
||||
@@ -61,7 +65,10 @@ func registerHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, u)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": u,
|
||||
"tokens": tokens,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// Package handler — 下载客户端管理 HTTP 端点。
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
// DownloadClientHandler 处理下载客户端的 CRUD 操作。
|
||||
type DownloadClientHandler struct {
|
||||
svc *service.Container
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// NewDownloadClientHandler 创建下载客户端处理器。
|
||||
func NewDownloadClientHandler(svc *service.Container, log *zap.Logger) *DownloadClientHandler {
|
||||
return &DownloadClientHandler{svc: svc, log: log}
|
||||
}
|
||||
|
||||
// downloadClientCreateRequest 创建下载客户端请求体。
|
||||
type downloadClientCreateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required,oneof=qbittorrent transmission aria2"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// downloadClientUpdateRequest 更新下载客户端请求体。
|
||||
type downloadClientUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type" binding:"omitempty,oneof=qbittorrent transmission aria2"`
|
||||
Host string `json:"host"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsDefault *bool `json:"is_default"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// Create 创建新的下载客户端。
|
||||
func (h *DownloadClientHandler) Create(c *gin.Context) {
|
||||
var req downloadClientCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrInvalidParams, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 加密密码
|
||||
password := req.Password
|
||||
if password != "" && h.svc.Crypto != nil {
|
||||
password = h.svc.Crypto.Encrypt(password)
|
||||
}
|
||||
|
||||
// 加密 Extra 配置
|
||||
extraStr := ""
|
||||
if len(req.Extra) > 0 {
|
||||
extraJSON, _ := json.Marshal(req.Extra)
|
||||
extraStr = string(extraJSON)
|
||||
if h.svc.Crypto != nil {
|
||||
extraStr = h.svc.Crypto.Encrypt(extraStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果设为默认,先清除其他默认
|
||||
if req.IsDefault {
|
||||
_ = h.svc.Repo.DownloadClient.ClearDefault(ctx)
|
||||
}
|
||||
|
||||
client := &model.DownloadClient{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Host: req.Host,
|
||||
Username: req.Username,
|
||||
Password: password,
|
||||
IsDefault: req.IsDefault,
|
||||
Enabled: true,
|
||||
Extra: extraStr,
|
||||
}
|
||||
|
||||
if err := h.svc.Repo.DownloadClient.Create(ctx, client); err != nil {
|
||||
Error(c, http.StatusInternalServerError, ErrInternal, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 热插拔:加载新客户端
|
||||
go func() {
|
||||
if initErr := h.svc.DownloadMgr.AddClient(ctx, client); initErr != nil {
|
||||
h.log.Warn("failed to hot-add download client", zap.Error(initErr))
|
||||
}
|
||||
}()
|
||||
|
||||
Success(c, client)
|
||||
}
|
||||
|
||||
// List 返回所有下载客户端。
|
||||
func (h *DownloadClientHandler) List(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
clients, err := h.svc.Repo.DownloadClient.List(ctx)
|
||||
if err != nil {
|
||||
Error(c, http.StatusInternalServerError, ErrInternal, "查询失败")
|
||||
return
|
||||
}
|
||||
Success(c, clients)
|
||||
}
|
||||
|
||||
// Get 返回指定下载客户端详情。
|
||||
func (h *DownloadClientHandler) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
client, err := h.svc.Repo.DownloadClient.FindByID(ctx, id)
|
||||
if err != nil || client == nil {
|
||||
Error(c, http.StatusNotFound, ErrNotFound, "客户端不存在")
|
||||
return
|
||||
}
|
||||
Success(c, client)
|
||||
}
|
||||
|
||||
// Update 更新下载客户端。
|
||||
func (h *DownloadClientHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req downloadClientUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrInvalidParams, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
client, err := h.svc.Repo.DownloadClient.FindByID(ctx, id)
|
||||
if err != nil || client == nil {
|
||||
Error(c, http.StatusNotFound, ErrNotFound, "客户端不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
client.Name = req.Name
|
||||
}
|
||||
if req.Type != "" {
|
||||
client.Type = req.Type
|
||||
}
|
||||
if req.Host != "" {
|
||||
client.Host = req.Host
|
||||
}
|
||||
if req.Username != "" {
|
||||
client.Username = req.Username
|
||||
}
|
||||
if req.Password != "" {
|
||||
if h.svc.Crypto != nil {
|
||||
client.Password = h.svc.Crypto.Encrypt(req.Password)
|
||||
} else {
|
||||
client.Password = req.Password
|
||||
}
|
||||
}
|
||||
if req.IsDefault != nil && *req.IsDefault {
|
||||
_ = h.svc.Repo.DownloadClient.ClearDefault(ctx)
|
||||
client.IsDefault = *req.IsDefault
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
client.Enabled = *req.Enabled
|
||||
}
|
||||
if len(req.Extra) > 0 {
|
||||
extraJSON, _ := json.Marshal(req.Extra)
|
||||
extraStr := string(extraJSON)
|
||||
if h.svc.Crypto != nil {
|
||||
client.Extra = h.svc.Crypto.Encrypt(extraStr)
|
||||
} else {
|
||||
client.Extra = extraStr
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.svc.Repo.DownloadClient.Update(ctx, client); err != nil {
|
||||
Error(c, http.StatusInternalServerError, ErrInternal, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 热更新适配器
|
||||
go func() {
|
||||
if updateErr := h.svc.DownloadMgr.UpdateClient(ctx, client); updateErr != nil {
|
||||
h.log.Warn("failed to hot-update download client", zap.Error(updateErr))
|
||||
}
|
||||
}()
|
||||
|
||||
Success(c, client)
|
||||
}
|
||||
|
||||
// Delete 删除下载客户端。
|
||||
func (h *DownloadClientHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
_, err := h.svc.Repo.DownloadClient.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
Error(c, http.StatusNotFound, ErrNotFound, "客户端不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if delErr := h.svc.Repo.DownloadClient.Delete(ctx, id); delErr != nil {
|
||||
Error(c, http.StatusInternalServerError, ErrInternal, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 热移除
|
||||
h.svc.DownloadMgr.RemoveClient(id)
|
||||
|
||||
SuccessWithMessage(c, "已删除", nil)
|
||||
}
|
||||
|
||||
// Test 测试下载客户端连接。
|
||||
func (h *DownloadClientHandler) Test(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
client, err := h.svc.Repo.DownloadClient.FindByID(ctx, id)
|
||||
if err != nil || client == nil {
|
||||
Error(c, http.StatusNotFound, ErrNotFound, "客户端不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.DownloadMgr.TestConnection(ctx, client); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrExternal, "连接测试失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessWithMessage(c, "连接成功", nil)
|
||||
}
|
||||
@@ -25,6 +25,7 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
|
||||
{
|
||||
auth.POST("/login", loginHandler(svc))
|
||||
auth.POST("/register", registerHandler(svc))
|
||||
auth.POST("/refresh", refreshHandler(svc))
|
||||
}
|
||||
|
||||
// Authenticated endpoints.
|
||||
@@ -34,6 +35,10 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
|
||||
authed.GET("/me", meHandler(svc))
|
||||
authed.PATCH("/me", updateProfileHandler(svc))
|
||||
authed.POST("/me/password", changePasswordHandler(svc))
|
||||
authed.POST("/me/logout", logoutHandler(svc))
|
||||
|
||||
// Permissions.
|
||||
authed.GET("/auth/permissions", getMyPermissionsHandler(svc))
|
||||
|
||||
// Libraries.
|
||||
authed.GET("/libraries", listLibrariesHandler(svc))
|
||||
@@ -129,6 +134,42 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
|
||||
authed.GET("/recycle", middleware.AdminRequired(), listRecycleHandler(svc))
|
||||
|
||||
authed.GET("/ws", wsHandler(svc))
|
||||
|
||||
// SSE event stream.
|
||||
authed.GET("/events", sseHandler(svc))
|
||||
|
||||
// Download clients.
|
||||
authed.GET("/download-clients", listDownloadClientsHandler(svc))
|
||||
authed.POST("/download-clients", middleware.AdminRequired(), createDownloadClientHandler(svc))
|
||||
authed.GET("/download-clients/:id", getDownloadClientHandler(svc))
|
||||
authed.PUT("/download-clients/:id", middleware.AdminRequired(), updateDownloadClientHandler(svc))
|
||||
authed.DELETE("/download-clients/:id", middleware.AdminRequired(), deleteDownloadClientHandler(svc))
|
||||
authed.POST("/download-clients/:id/test", middleware.AdminRequired(), testDownloadClientHandler(svc))
|
||||
|
||||
// Notify channels.
|
||||
authed.GET("/notify-channels", listNotifyChannelsHandler(svc))
|
||||
authed.GET("/notify-channels/types", getNotifyChannelTypesHandler(svc))
|
||||
authed.POST("/notify-channels", middleware.AdminRequired(), createNotifyChannelHandler(svc))
|
||||
authed.GET("/notify-channels/:id", getNotifyChannelHandler(svc))
|
||||
authed.PUT("/notify-channels/:id", middleware.AdminRequired(), updateNotifyChannelHandler(svc))
|
||||
authed.DELETE("/notify-channels/:id", middleware.AdminRequired(), deleteNotifyChannelHandler(svc))
|
||||
authed.POST("/notify-channels/:id/test", middleware.AdminRequired(), testNotifyChannelHandler(svc))
|
||||
|
||||
// Scheduler.
|
||||
authed.GET("/scheduler/tasks", schedulerListTasksHandler(svc))
|
||||
authed.POST("/scheduler/tasks/:id/run", middleware.AdminRequired(), schedulerRunTaskHandler(svc))
|
||||
authed.GET("/scheduler/status", schedulerGetStatusHandler(svc))
|
||||
|
||||
// Sites (PT 站点管理).
|
||||
siteHandler := NewSiteHandler(svc)
|
||||
authed.GET("/sites", siteHandler.ListSites)
|
||||
authed.GET("/sites/types", siteHandler.GetSiteTypes)
|
||||
authed.GET("/sites/auth-types", siteHandler.GetAuthTypes)
|
||||
authed.POST("/sites", middleware.AdminRequired(), siteHandler.CreateSite)
|
||||
authed.GET("/sites/:id", siteHandler.GetSite)
|
||||
authed.PUT("/sites/:id", middleware.AdminRequired(), siteHandler.UpdateSite)
|
||||
authed.DELETE("/sites/:id", middleware.AdminRequired(), siteHandler.DeleteSite)
|
||||
authed.POST("/sites/:id/test", middleware.AdminRequired(), siteHandler.TestSite)
|
||||
}
|
||||
|
||||
// Admin-only endpoints.
|
||||
@@ -164,6 +205,24 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
|
||||
// Scheduled jobs.
|
||||
admin.GET("/scheduler", schedulerStatusHandler(svc))
|
||||
admin.POST("/scheduler/:name/run", schedulerRunHandler(svc))
|
||||
|
||||
// User permissions management.
|
||||
admin.GET("/users/:id/permissions", getUserPermissionsHandler(svc))
|
||||
admin.PUT("/users/:id/permissions", updateUserPermissionsHandler(svc))
|
||||
admin.POST("/users/:id/permissions/reset", resetUserPermissionsHandler(svc))
|
||||
}
|
||||
|
||||
// API Config management (admin only).
|
||||
apiConfig := api.Group("/api-config")
|
||||
apiConfig.Use(middleware.AuthRequired(cfg.Secrets.JWTSecret), middleware.AdminRequired())
|
||||
{
|
||||
apiConfig.GET("", listApiConfigsHandler(svc))
|
||||
apiConfig.GET("/providers/list", listProvidersHandler(svc))
|
||||
apiConfig.GET("/:provider", getApiConfigHandler(svc))
|
||||
apiConfig.GET("/:provider/effective", getEffectiveConfigHandler(svc))
|
||||
apiConfig.POST("/:provider", upsertApiConfigHandler(svc))
|
||||
apiConfig.DELETE("/:provider", deleteApiConfigHandler(svc))
|
||||
apiConfig.POST("/:provider/test", testApiConfigHandler(svc))
|
||||
}
|
||||
|
||||
// Emby/Jellyfin compatibility shim (read-only).
|
||||
@@ -188,3 +247,198 @@ func healthCheck(c *gin.Context) {
|
||||
func versionInfo(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"name": "MediaStationGo", "version": "0.1.0"})
|
||||
}
|
||||
|
||||
// ─── 权限 Handler 包装 ────────────────────────────────────────────────────────
|
||||
|
||||
func getUserPermissionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewPermissionHandler(svc, svc.Log)
|
||||
return h.GetUserPermissions
|
||||
}
|
||||
|
||||
func updateUserPermissionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewPermissionHandler(svc, svc.Log)
|
||||
return h.UpdateUserPermissions
|
||||
}
|
||||
|
||||
func resetUserPermissionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewPermissionHandler(svc, svc.Log)
|
||||
return h.ResetUserPermissions
|
||||
}
|
||||
|
||||
func getMyPermissionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewPermissionHandler(svc, svc.Log)
|
||||
return h.GetMyPermissions
|
||||
}
|
||||
|
||||
// ─── 刷新 Handler 包装 ────────────────────────────────────────────────────────
|
||||
|
||||
func refreshHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewRefreshHandler(svc, svc.Log)
|
||||
return h.RefreshToken
|
||||
}
|
||||
|
||||
func logoutHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewRefreshHandler(svc, svc.Log)
|
||||
return h.Logout
|
||||
}
|
||||
|
||||
// ─── API Config Handler 包装 ───────────────────────────────────────────────────
|
||||
|
||||
func listApiConfigsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewApiConfigHandler(svc, svc.Log)
|
||||
return h.ListApiConfigs
|
||||
}
|
||||
|
||||
func listProvidersHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewApiConfigHandler(svc, svc.Log)
|
||||
return h.ListProviders
|
||||
}
|
||||
|
||||
func getApiConfigHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewApiConfigHandler(svc, svc.Log)
|
||||
return h.GetApiConfig
|
||||
}
|
||||
|
||||
func getEffectiveConfigHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewApiConfigHandler(svc, svc.Log)
|
||||
return h.GetEffectiveConfig
|
||||
}
|
||||
|
||||
func upsertApiConfigHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewApiConfigHandler(svc, svc.Log)
|
||||
return h.UpsertApiConfig
|
||||
}
|
||||
|
||||
func deleteApiConfigHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewApiConfigHandler(svc, svc.Log)
|
||||
return h.DeleteApiConfig
|
||||
}
|
||||
|
||||
func testApiConfigHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewApiConfigHandler(svc, svc.Log)
|
||||
return h.TestApiConfig
|
||||
}
|
||||
|
||||
// ─── Download Client Handler 包装 ─────────────────────────────────────────────
|
||||
|
||||
func listDownloadClientsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewDownloadClientHandler(svc, svc.Log)
|
||||
return h.List
|
||||
}
|
||||
|
||||
func createDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewDownloadClientHandler(svc, svc.Log)
|
||||
return h.Create
|
||||
}
|
||||
|
||||
func getDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewDownloadClientHandler(svc, svc.Log)
|
||||
return h.Get
|
||||
}
|
||||
|
||||
func updateDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewDownloadClientHandler(svc, svc.Log)
|
||||
return h.Update
|
||||
}
|
||||
|
||||
func deleteDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewDownloadClientHandler(svc, svc.Log)
|
||||
return h.Delete
|
||||
}
|
||||
|
||||
func testDownloadClientHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewDownloadClientHandler(svc, svc.Log)
|
||||
return h.Test
|
||||
}
|
||||
|
||||
// ─── Notify Channel Handler 包装 ──────────────────────────────────────────────
|
||||
|
||||
func listNotifyChannelsHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewNotifyHandler(svc, svc.Log)
|
||||
return h.List
|
||||
}
|
||||
|
||||
func getNotifyChannelTypesHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewNotifyHandler(svc, svc.Log)
|
||||
return h.GetTypes
|
||||
}
|
||||
|
||||
func createNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewNotifyHandler(svc, svc.Log)
|
||||
return h.Create
|
||||
}
|
||||
|
||||
func getNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewNotifyHandler(svc, svc.Log)
|
||||
return h.Get
|
||||
}
|
||||
|
||||
func updateNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewNotifyHandler(svc, svc.Log)
|
||||
return h.Update
|
||||
}
|
||||
|
||||
func deleteNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewNotifyHandler(svc, svc.Log)
|
||||
return h.Delete
|
||||
}
|
||||
|
||||
func testNotifyChannelHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewNotifyHandler(svc, svc.Log)
|
||||
return h.Test
|
||||
}
|
||||
|
||||
// ─── Scheduler Handler 包装 ──────────────────────────────────────────────────
|
||||
|
||||
func schedulerListTasksHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewSchedulerHandler(svc, svc.Log)
|
||||
return h.ListTasks
|
||||
}
|
||||
|
||||
func schedulerRunTaskHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewSchedulerHandler(svc, svc.Log)
|
||||
return h.RunTask
|
||||
}
|
||||
|
||||
func schedulerGetStatusHandler(svc *service.Container) gin.HandlerFunc {
|
||||
h := NewSchedulerHandler(svc, svc.Log)
|
||||
return h.GetStatus
|
||||
}
|
||||
|
||||
// ─── SSE Handler ──────────────────────────────────────────────────────────────
|
||||
|
||||
func sseHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 获取 SSE Hub
|
||||
hub := svc.SSEHub
|
||||
|
||||
// 设置 SSE 响应头
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
|
||||
// 订阅事件流
|
||||
client := hub.Subscribe()
|
||||
defer hub.Unsubscribe(client)
|
||||
|
||||
// 发送初始连接成功事件
|
||||
c.SSEvent("connected", gin.H{"status": "ok"})
|
||||
c.Writer.Flush()
|
||||
|
||||
// 持续发送事件直到客户端断开连接
|
||||
clientGone := c.Request.Context().Done()
|
||||
for {
|
||||
select {
|
||||
case <-clientGone:
|
||||
return
|
||||
case event, ok := <-client.Ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.SSEvent(event.Type, event.Payload)
|
||||
c.Writer.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// Package handler — 通知渠道管理 HTTP 端点。
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
// NotifyHandler 处理通知渠道的 CRUD 操作。
|
||||
type NotifyHandler struct {
|
||||
svc *service.Container
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// NewNotifyHandler 创建通知渠道处理器。
|
||||
func NewNotifyHandler(svc *service.Container, log *zap.Logger) *NotifyHandler {
|
||||
return &NotifyHandler{svc: svc, log: log}
|
||||
}
|
||||
|
||||
// notifyCreateRequest 创建通知渠道请求体。
|
||||
type notifyCreateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required,oneof=telegram wechat bark webhook email"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Config map[string]string `json:"config" binding:"required"`
|
||||
Events []string `json:"events"`
|
||||
}
|
||||
|
||||
// notifyUpdateRequest 更新通知渠道请求体。
|
||||
type notifyUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Config map[string]string `json:"config"`
|
||||
Events []string `json:"events"`
|
||||
}
|
||||
|
||||
// Create 创建新的通知渠道。
|
||||
func (h *NotifyHandler) Create(c *gin.Context) {
|
||||
var req notifyCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrInvalidParams, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 验证配置
|
||||
if err := h.svc.Notify.ValidateChannelConfig(req.Type, req.Config); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrInvalidParams, "配置验证失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 加密配置
|
||||
configJSON, _ := json.Marshal(req.Config)
|
||||
configStr := string(configJSON)
|
||||
if h.svc.Crypto != nil {
|
||||
configStr = h.svc.Crypto.Encrypt(configStr)
|
||||
}
|
||||
|
||||
// 序列化事件列表
|
||||
eventsJSON, _ := json.Marshal(req.Events)
|
||||
eventsStr := string(eventsJSON)
|
||||
|
||||
channel := &model.NotifyChannel{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Enabled: req.Enabled,
|
||||
Config: configStr,
|
||||
Events: eventsStr,
|
||||
}
|
||||
|
||||
if err := h.svc.Repo.NotifyChannel.Create(ctx, channel); err != nil {
|
||||
Error(c, http.StatusInternalServerError, ErrInternal, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(c, channel)
|
||||
}
|
||||
|
||||
// List 返回所有通知渠道。
|
||||
func (h *NotifyHandler) List(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
channels, err := h.svc.Repo.NotifyChannel.List(ctx)
|
||||
if err != nil {
|
||||
Error(c, http.StatusInternalServerError, ErrInternal, "查询失败")
|
||||
return
|
||||
}
|
||||
Success(c, channels)
|
||||
}
|
||||
|
||||
// Get 返回指定通知渠道详情。
|
||||
func (h *NotifyHandler) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
channel, err := h.svc.Repo.NotifyChannel.FindByID(ctx, id)
|
||||
if err != nil || channel == nil {
|
||||
Error(c, http.StatusNotFound, ErrNotFound, "通知渠道不存在")
|
||||
return
|
||||
}
|
||||
Success(c, channel)
|
||||
}
|
||||
|
||||
// Update 更新通知渠道。
|
||||
func (h *NotifyHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req notifyUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrInvalidParams, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
channel, err := h.svc.Repo.NotifyChannel.FindByID(ctx, id)
|
||||
if err != nil || channel == nil {
|
||||
Error(c, http.StatusNotFound, ErrNotFound, "通知渠道不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
channel.Name = req.Name
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
channel.Enabled = *req.Enabled
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
if len(req.Config) > 0 {
|
||||
if err := h.svc.Notify.ValidateChannelConfig(channel.Type, req.Config); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrInvalidParams, "配置验证失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
configJSON, _ := json.Marshal(req.Config)
|
||||
configStr := string(configJSON)
|
||||
if h.svc.Crypto != nil {
|
||||
configStr = h.svc.Crypto.Encrypt(configStr)
|
||||
}
|
||||
channel.Config = configStr
|
||||
}
|
||||
|
||||
// 更新事件列表
|
||||
if req.Events != nil {
|
||||
eventsJSON, _ := json.Marshal(req.Events)
|
||||
channel.Events = string(eventsJSON)
|
||||
}
|
||||
|
||||
if err := h.svc.Repo.NotifyChannel.Update(ctx, channel); err != nil {
|
||||
Error(c, http.StatusInternalServerError, ErrInternal, "更新失败")
|
||||
return
|
||||
}
|
||||
|
||||
Success(c, channel)
|
||||
}
|
||||
|
||||
// Delete 删除通知渠道。
|
||||
func (h *NotifyHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
_, err := h.svc.Repo.NotifyChannel.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
Error(c, http.StatusNotFound, ErrNotFound, "通知渠道不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if delErr := h.svc.Repo.NotifyChannel.Delete(ctx, id); delErr != nil {
|
||||
Error(c, http.StatusInternalServerError, ErrInternal, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
SuccessWithMessage(c, "已删除", nil)
|
||||
}
|
||||
|
||||
// Test 发送测试通知。
|
||||
func (h *NotifyHandler) Test(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
if err := h.svc.Notify.SendTest(ctx, id); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrExternal, "测试通知发送失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessWithMessage(c, "测试通知已发送", nil)
|
||||
}
|
||||
|
||||
// GetTypes 返回支持的通知渠道类型列表。
|
||||
func (h *NotifyHandler) GetTypes(c *gin.Context) {
|
||||
types := h.svc.Notify.GetProviderTypes()
|
||||
Success(c, types)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Package handler — 权限相关 HTTP Handler。
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
// PermissionHandler 权限相关 HTTP 处理。
|
||||
type PermissionHandler struct {
|
||||
svc *service.Container
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// NewPermissionHandler 创建权限处理器。
|
||||
func NewPermissionHandler(svc *service.Container, log *zap.Logger) *PermissionHandler {
|
||||
return &PermissionHandler{svc: svc, log: log}
|
||||
}
|
||||
|
||||
// GetUserPermissions 获取指定用户的权限(管理员)。
|
||||
// GET /api/users/:id/permissions
|
||||
func (h *PermissionHandler) GetUserPermissions(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "user id required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// 权限检查:需要管理员权限
|
||||
role := middleware.GetUserRole(c)
|
||||
if role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 40301, "message": "admin only", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
perms, err := h.svc.Permission.GetByUserID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.log.Error("get user permissions failed", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": perms, "message": "ok"})
|
||||
}
|
||||
|
||||
// UpdateUserPermissions 更新指定用户的权限(管理员)。
|
||||
// PUT /api/users/:id/permissions
|
||||
func (h *PermissionHandler) UpdateUserPermissions(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "user id required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// 权限检查:需要管理员权限
|
||||
role := middleware.GetUserRole(c)
|
||||
if role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 40301, "message": "admin only", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Permissions map[string]bool `json:"permissions"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "invalid request", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Permission.Update(c.Request.Context(), userID, req.Permissions); err != nil {
|
||||
h.log.Error("update user permissions failed", zap.Error(err), zap.String("user_id", userID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "permissions updated", "data": nil})
|
||||
}
|
||||
|
||||
// ResetUserPermissions 重置指定用户的权限为默认值(管理员)。
|
||||
// POST /api/users/:id/permissions/reset
|
||||
func (h *PermissionHandler) ResetUserPermissions(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "user id required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// 权限检查:需要管理员权限
|
||||
role := middleware.GetUserRole(c)
|
||||
if role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 40301, "message": "admin only", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Permission.ResetToDefault(c.Request.Context(), userID); err != nil {
|
||||
h.log.Error("reset user permissions failed", zap.Error(err), zap.String("user_id", userID))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "permissions reset to default", "data": nil})
|
||||
}
|
||||
|
||||
// GetMyPermissions 获取当前用户的权限。
|
||||
// GET /api/auth/permissions
|
||||
func (h *PermissionHandler) GetMyPermissions(c *gin.Context) {
|
||||
currentUserID := middleware.GetUserID(c)
|
||||
if currentUserID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 40101, "message": "authentication required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
perms, err := h.svc.Permission.GetPermissionMap(c.Request.Context(), currentUserID)
|
||||
if err != nil {
|
||||
h.log.Error("get my permissions failed", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// 同时返回角色和等级信息
|
||||
role := middleware.GetUserRole(c)
|
||||
tier := middleware.GetUserTier(c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": gin.H{
|
||||
"permissions": perms,
|
||||
"role": role,
|
||||
"tier": tier,
|
||||
"is_super": role == "admin" || tier == "plus",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Package handler — 令牌刷新 HTTP Handler。
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
// RefreshHandler 令牌刷新 HTTP 处理。
|
||||
type RefreshHandler struct {
|
||||
svc *service.Container
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// NewRefreshHandler 创建刷新令牌处理器。
|
||||
func NewRefreshHandler(svc *service.Container, log *zap.Logger) *RefreshHandler {
|
||||
return &RefreshHandler{svc: svc, log: log}
|
||||
}
|
||||
|
||||
// RefreshTokenRequest 刷新令牌请求结构。
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
|
||||
// RefreshToken 刷新访问令牌。
|
||||
// POST /api/auth/refresh
|
||||
func (h *RefreshHandler) RefreshToken(c *gin.Context) {
|
||||
var req RefreshTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 40001, "message": "refresh_token required", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := h.svc.Auth.RefreshTokens(c.Request.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
h.log.Debug("token refresh failed", zap.Error(err))
|
||||
|
||||
// 根据错误类型返回不同状态码
|
||||
switch err {
|
||||
case service.ErrInvalidRefreshToken:
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 40101, "message": "invalid refresh token", "data": nil})
|
||||
case service.ErrTokenExpired:
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 40102, "message": "refresh token expired", "data": nil})
|
||||
case service.ErrTokenRevoked:
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 40103, "message": "refresh token revoked", "data": nil})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": gin.H{
|
||||
"token": tokens.AccessToken,
|
||||
"refresh_token": tokens.RefreshToken,
|
||||
"expires_in": tokens.ExpiresIn,
|
||||
"token_type": tokens.TokenType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Logout 登出当前用户。
|
||||
// POST /api/auth/logout
|
||||
func (h *RefreshHandler) Logout(c *gin.Context) {
|
||||
userID := c.GetString("ctx_user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Auth.Logout(c.Request.Context(), userID); err != nil {
|
||||
h.log.Warn("logout failed", zap.Error(err), zap.String("user_id", userID))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": nil})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Package handler — 统一响应格式和错误码。
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ─── 错误码定义 ───────────────────────────────────────────────────────────────
|
||||
|
||||
// 统一错误码
|
||||
const (
|
||||
ErrOK = 0
|
||||
ErrInvalidParams = 40001
|
||||
ErrUnauthorized = 40101
|
||||
ErrForbidden = 40301
|
||||
ErrNotFound = 40401
|
||||
ErrConflict = 40901
|
||||
ErrInternal = 50001
|
||||
ErrExternal = 50201
|
||||
ErrEncryptFailed = 50801
|
||||
)
|
||||
|
||||
// ─── Response Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
// APIResponse 统一 API 响应格式。
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// PaginatedResponse 分页响应格式。
|
||||
type PaginatedResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// Success 返回成功响应。
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, APIResponse{Code: 0, Message: "ok", Data: data})
|
||||
}
|
||||
|
||||
// SuccessWithMessage 返回带消息的成功响应。
|
||||
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
|
||||
c.JSON(http.StatusOK, APIResponse{Code: 0, Message: message, Data: data})
|
||||
}
|
||||
|
||||
// Error 返回错误响应。
|
||||
func Error(c *gin.Context, httpStatus int, code int, message string) {
|
||||
c.JSON(httpStatus, APIResponse{Code: code, Message: message, Data: nil})
|
||||
}
|
||||
|
||||
// Paginated 返回分页响应。
|
||||
func Paginated(c *gin.Context, items interface{}, total int64, page, pageSize int) {
|
||||
c.JSON(http.StatusOK, PaginatedResponse{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Package handler — 定时任务管理 HTTP 端点。
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
// SchedulerHandler 处理定时任务的查询和管理操作。
|
||||
type SchedulerHandler struct {
|
||||
svc *service.Container
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// NewSchedulerHandler 创建定时任务处理器。
|
||||
func NewSchedulerHandler(svc *service.Container, log *zap.Logger) *SchedulerHandler {
|
||||
return &SchedulerHandler{svc: svc, log: log}
|
||||
}
|
||||
|
||||
// ListTasks 返回所有定时任务列表。
|
||||
func (h *SchedulerHandler) ListTasks(c *gin.Context) {
|
||||
tasks := h.svc.Scheduler.Status()
|
||||
Success(c, tasks)
|
||||
}
|
||||
|
||||
// RunTask 手动触发指定任务。
|
||||
func (h *SchedulerHandler) RunTask(c *gin.Context) {
|
||||
name := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
if err := h.svc.Scheduler.RunNow(ctx, name); err != nil {
|
||||
Error(c, http.StatusBadRequest, ErrInternal, "任务执行失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessWithMessage(c, "任务已触发执行", nil)
|
||||
}
|
||||
|
||||
// GetStatus 返回调度器运行状态。
|
||||
func (h *SchedulerHandler) GetStatus(c *gin.Context) {
|
||||
status := h.svc.Scheduler.Status()
|
||||
Success(c, status)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Package handler — PT 站点管理 HTTP 处理。
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
// SiteHandler 站点管理 CRUD。
|
||||
type SiteHandler struct {
|
||||
svc *service.Container
|
||||
}
|
||||
|
||||
// NewSiteHandler 创建站点管理 Handler。
|
||||
func NewSiteHandler(svc *service.Container) *SiteHandler {
|
||||
return &SiteHandler{svc: svc}
|
||||
}
|
||||
|
||||
// ListSites 列出所有站点。
|
||||
func (h *SiteHandler) ListSites(c *gin.Context) {
|
||||
sites, err := h.svc.Site.List(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
if sites == nil {
|
||||
sites = []model.Site{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": sites})
|
||||
}
|
||||
|
||||
// GetSite 获取单个站点详情(解密敏感字段)。
|
||||
func (h *SiteHandler) GetSite(c *gin.Context) {
|
||||
site, err := h.svc.Site.GetByID(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
if err == service.ErrSiteNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "site not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": site})
|
||||
}
|
||||
|
||||
// CreateSite 创建站点。
|
||||
func (h *SiteHandler) CreateSite(c *gin.Context) {
|
||||
var site model.Site
|
||||
if err := c.ShouldBindJSON(&site); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
created, err := h.svc.Site.Create(c.Request.Context(), &site)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"code": 0, "message": "ok", "data": created})
|
||||
}
|
||||
|
||||
// UpdateSite 更新站点。
|
||||
func (h *SiteHandler) UpdateSite(c *gin.Context) {
|
||||
var site model.Site
|
||||
if err := c.ShouldBindJSON(&site); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
site.ID = c.Param("id")
|
||||
updated, err := h.svc.Site.Update(c.Request.Context(), &site)
|
||||
if err != nil {
|
||||
if err == service.ErrSiteNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "site not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": updated})
|
||||
}
|
||||
|
||||
// DeleteSite 删除站点。
|
||||
func (h *SiteHandler) DeleteSite(c *gin.Context) {
|
||||
if err := h.svc.Site.Delete(c.Request.Context(), c.Param("id")); err != nil {
|
||||
if err == service.ErrSiteNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 1, "message": "site not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
|
||||
}
|
||||
|
||||
// TestSite 测试站点连通性。
|
||||
func (h *SiteHandler) TestSite(c *gin.Context) {
|
||||
if err := h.svc.Site.Authenticate(c.Request.Context(), c.Param("id")); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
|
||||
}
|
||||
|
||||
// GetSiteTypes 返回支持的站点类型列表。
|
||||
func (h *SiteHandler) GetSiteTypes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": model.SiteTypes()})
|
||||
}
|
||||
|
||||
// GetAuthTypes 返回支持的认证方式列表。
|
||||
func (h *SiteHandler) GetAuthTypes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": model.AuthTypes()})
|
||||
}
|
||||
Reference in New Issue
Block a user