mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-03 12:26:36 +08:00
fix(subscription): prevent duplicate qb downloads
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
func activeUserRequired(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
uid, _ := c.Get(middleware.CtxUserID)
|
||||
userID, _ := uid.(string)
|
||||
if userID == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 40101, "message": "missing user"})
|
||||
return
|
||||
}
|
||||
u, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil || u == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 40101, "message": "user not found"})
|
||||
return
|
||||
}
|
||||
if !u.IsActive {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": 40302, "message": "user account is disabled"})
|
||||
return
|
||||
}
|
||||
if u.ExpiredAt != nil && time.Now().After(*u.ExpiredAt) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": 40303, "message": "user account has expired"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func activeEmbyUserRequired(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
uid, _ := c.Get(middleware.CtxUserID)
|
||||
userID, _ := uid.(string)
|
||||
u, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if userID == "" || err != nil || u == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"Code": 40101, "Message": "User not found"})
|
||||
return
|
||||
}
|
||||
if !u.IsActive {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"Code": 40302, "Message": "User account is disabled"})
|
||||
return
|
||||
}
|
||||
if u.ExpiredAt != nil && time.Now().After(*u.ExpiredAt) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"Code": 40303, "Message": "User account has expired"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,10 @@ type adminResetPasswordReq struct {
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
type adminUpdateUserStatusReq struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func updateUserHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req adminUpdateUserReq
|
||||
@@ -151,6 +155,46 @@ func resetUserPasswordHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func updateUserStatusHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req adminUpdateUserStatusReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
userID := c.Param("id")
|
||||
if !req.IsActive {
|
||||
if firstAdmin, err := svc.Repo.User.FirstAdmin(c.Request.Context()); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else if firstAdmin != nil && firstAdmin.ID == userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "default admin cannot be disabled"})
|
||||
return
|
||||
}
|
||||
}
|
||||
updates := map[string]any{"is_active": req.IsActive}
|
||||
if req.IsActive {
|
||||
updates["share_warnings"] = 0
|
||||
updates["last_share_warn_at"] = nil
|
||||
}
|
||||
if err := svc.Repo.User.UpdateFields(c.Request.Context(), userID, updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.IsActive {
|
||||
_ = svc.Repo.UserDevice.SetKickedByUser(c.Request.Context(), userID, false)
|
||||
} else {
|
||||
_ = svc.Repo.UserDevice.SetKickedByUser(c.Request.Context(), userID, true)
|
||||
}
|
||||
updated, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, updated)
|
||||
}
|
||||
}
|
||||
|
||||
func annotateProtectedUsers(ctx context.Context, svc *service.Container, users []model.User) error {
|
||||
firstAdmin, err := svc.Repo.User.FirstAdmin(ctx)
|
||||
if err != nil || firstAdmin == nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -73,6 +74,10 @@ func addDownloadHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}, fallbackTitle, "")
|
||||
t, err := svc.Downloads.AddDownloadWithMeta(c.Request.Context(), uid.(string), realURL, req.SavePath, meta)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrDownloadAlreadyExists) {
|
||||
c.JSON(http.StatusOK, t)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -779,7 +779,7 @@ func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container)
|
||||
grp.HEAD("/Items/:id/Images/:type", embyItemImageHandler(svc))
|
||||
|
||||
// 鉴权后端点
|
||||
auth := grp.Group("", middleware.EmbyAuthRequired(jwtSecret))
|
||||
auth := grp.Group("", middleware.EmbyAuthRequired(jwtSecret), activeEmbyUserRequired(svc))
|
||||
auth.GET("/Users/Me", embyMeHandler(svc))
|
||||
auth.GET("/Users", embyListUsersHandler(svc))
|
||||
auth.GET("/Users/:userId", embyGetUserByIDHandler(svc))
|
||||
|
||||
@@ -92,10 +92,20 @@ func TestEmbyVirtualFoldersRouteReturnsJSON(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.Library{}); err != nil {
|
||||
if err := db.AutoMigrate(&model.User{}, &model.Library{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
if err := repos.User.Create(t.Context(), &model.User{
|
||||
Base: model.Base{ID: "user-1"},
|
||||
Username: "tester",
|
||||
PasswordHash: "x",
|
||||
Role: "admin",
|
||||
Tier: "plus",
|
||||
IsActive: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
for _, lib := range []model.Library{
|
||||
{Name: "电影", Path: "D:\\media\\movies", Type: "movie", Enabled: true},
|
||||
{Name: "剧集", Path: "D:\\media\\tv", Type: "tv", Enabled: true},
|
||||
@@ -201,10 +211,20 @@ func TestEmbyUserItemByIDRouteReturnsJSON(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.Library{}, &model.Media{}, &model.Favorite{}, &model.PlaybackHistory{}); err != nil {
|
||||
if err := db.AutoMigrate(&model.User{}, &model.Library{}, &model.Media{}, &model.Favorite{}, &model.PlaybackHistory{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
if err := repos.User.Create(t.Context(), &model.User{
|
||||
Base: model.Base{ID: "user-1"},
|
||||
Username: "tester",
|
||||
PasswordHash: "x",
|
||||
Role: "admin",
|
||||
Tier: "plus",
|
||||
IsActive: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
lib := model.Library{Name: "剧集", Path: "D:\\media\\tv", Type: "tv", Enabled: true}
|
||||
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
||||
t.Fatalf("create library: %v", err)
|
||||
|
||||
@@ -50,6 +50,7 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
|
||||
// Authenticated endpoints.
|
||||
authed := api.Group("/")
|
||||
authed.Use(middleware.AuthRequired(cfg.Secrets.JWTSecret))
|
||||
authed.Use(activeUserRequired(svc))
|
||||
{
|
||||
authed.GET("/me", meHandler(svc))
|
||||
authed.PATCH("/me", updateProfileHandler(svc))
|
||||
@@ -298,6 +299,7 @@ func Register(r *gin.Engine, cfg *config.Config, log *zap.Logger, svc *service.C
|
||||
admin.POST("/users", createUserHandler(svc))
|
||||
admin.PATCH("/users/:id", updateUserHandler(svc))
|
||||
admin.PATCH("/users/:id/password", resetUserPasswordHandler(svc))
|
||||
admin.PATCH("/users/:id/status", updateUserStatusHandler(svc))
|
||||
admin.PATCH("/users/:id/role", adminUpdateRoleHandler(svc))
|
||||
admin.DELETE("/users/:id", deleteUserHandler(svc))
|
||||
admin.GET("/settings", listSettingsHandler(svc))
|
||||
|
||||
@@ -47,6 +47,10 @@ func (h *RefreshHandler) RefreshToken(c *gin.Context) {
|
||||
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})
|
||||
case service.ErrUserInactive:
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 40302, "message": "user account is disabled", "data": nil})
|
||||
case service.ErrUserExpired:
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 40303, "message": "user account has expired", "data": nil})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 50001, "message": "internal error", "data": nil})
|
||||
}
|
||||
@@ -54,7 +58,7 @@ func (h *RefreshHandler) RefreshToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": gin.H{
|
||||
"token": tokens.AccessToken,
|
||||
|
||||
Reference in New Issue
Block a user