mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
feat: add licensing and access controls
This commit is contained in:
@@ -2,10 +2,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
||||
)
|
||||
|
||||
@@ -16,12 +20,110 @@ func listUsersHandler(svc *service.Container) gin.HandlerFunc {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := annotateProtectedUsers(c.Request.Context(), svc, users); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, users)
|
||||
}
|
||||
}
|
||||
|
||||
type adminCreateUserReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func createUserHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req adminCreateUserReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
u, _, err := svc.Auth.Register(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
writeUserMutationError(c, err)
|
||||
return
|
||||
}
|
||||
// Admin-created users are intentionally normal viewers by default.
|
||||
// They can log in from Web/Emby-compatible clients and play media, but
|
||||
// cannot scrape, scan, download, delete, export NFO, or manage files.
|
||||
if u.Role != "user" {
|
||||
u, err = svc.Profile.AdminUpdateRole(c.Request.Context(), u.ID, "user")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusCreated, u)
|
||||
}
|
||||
}
|
||||
|
||||
type adminUpdateUserReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
}
|
||||
|
||||
func updateUserHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req adminUpdateUserReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
nextUsername := strings.TrimSpace(req.Username)
|
||||
if nextUsername == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "username required"})
|
||||
return
|
||||
}
|
||||
userID := c.Param("id")
|
||||
user, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
if existing, err := svc.Repo.User.FindByUsername(c.Request.Context(), nextUsername); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else if existing != nil && existing.ID != userID {
|
||||
writeUserMutationError(c, service.ErrUsernameTaken)
|
||||
return
|
||||
}
|
||||
updates := map[string]any{"username": nextUsername}
|
||||
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 {
|
||||
updates["role"] = "admin"
|
||||
updates["tier"] = "plus"
|
||||
}
|
||||
if err := svc.Repo.User.UpdateFields(c.Request.Context(), userID, updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
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 deleteUserHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
firstAdmin, err := svc.Repo.User.FirstAdmin(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if firstAdmin != nil && firstAdmin.ID == c.Param("id") {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "default admin cannot be deleted"})
|
||||
return
|
||||
}
|
||||
if err := svc.Repo.User.Delete(c.Request.Context(), c.Param("id")); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -30,6 +132,33 @@ func deleteUserHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func annotateProtectedUsers(ctx context.Context, svc *service.Container, users []model.User) error {
|
||||
firstAdmin, err := svc.Repo.User.FirstAdmin(ctx)
|
||||
if err != nil || firstAdmin == nil {
|
||||
return err
|
||||
}
|
||||
for i := range users {
|
||||
if users[i].ID == firstAdmin.ID {
|
||||
users[i].IsDefaultAdmin = true
|
||||
users[i].IsProtected = true
|
||||
users[i].Role = "admin"
|
||||
users[i].Tier = "plus"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeUserMutationError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrUsernameTaken):
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "username already taken"})
|
||||
case errors.Is(err, service.ErrUserLimitReached):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user limit reached: maximum 20 users"})
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
type settingReq struct {
|
||||
Key string `json:"key" binding:"required"`
|
||||
Value string `json:"value"`
|
||||
|
||||
Reference in New Issue
Block a user