mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
b0fe40142a
* Rebrand MMTL to MeBox across codebase and assets Rename the project display name, Go module path, environment variable prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl to MeBox/mebox. Replace logo assets with the new MeBox icon and keep legacy SQLite migration support for existing mmtl.db deployments. Co-authored-by: truewhile <truewhile@users.noreply.github.com> * Fix logo icons: use cube-only crop without truncated text Previous icon generation cropped too much of the source image, including partial MeBox wordmark text that was cut off in square icon containers. Regenerate logo-64/192/512, favicon, and SVG from cube-only region. Co-authored-by: truewhile <truewhile@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
175 lines
5.4 KiB
Go
175 lines
5.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
// ─── Users / Auth ────────────────────────────────────────────────────────────
|
|
|
|
// embyAuthByNameHandler 处理 POST /Users/AuthenticateByName。
|
|
//
|
|
// 这是 Emby 客户端登录的唯一入口(Infuse / Yamby / Hills 等都走这里)。
|
|
// 用户名+密码 → 调用我们已有的 AuthService.Login → 返回 AccessToken + User。
|
|
func embyAuthByNameHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
req, err := parseEmbyAuthByNameReq(c)
|
|
if err != nil {
|
|
embyError(c, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
password := req.Pw
|
|
if password == "" {
|
|
password = req.Password
|
|
}
|
|
if strings.TrimSpace(req.Username) == "" || password == "" {
|
|
if req.PasswordMd5 != "" || req.PasswordSha1 != "" {
|
|
embyError(c, http.StatusBadRequest, "plain password required")
|
|
return
|
|
}
|
|
embyError(c, http.StatusBadRequest, "missing username or password")
|
|
return
|
|
}
|
|
resp, err := svc.Auth.Login(c.Request.Context(), req.Username, password)
|
|
if err != nil {
|
|
embyError(c, http.StatusUnauthorized, err.Error())
|
|
return
|
|
}
|
|
// 记录登录设备会话并执行防共享检测(登录客户端数 / 设备指纹)。
|
|
clientInfo := embyClientInfoFromRequest(c)
|
|
if svc.Sessions != nil {
|
|
svc.Sessions.RecordLogin(c.Request.Context(), resp.User.ID, resp.User.Username,
|
|
clientInfo.DeviceID,
|
|
clientInfo.DeviceName,
|
|
clientInfo.Client,
|
|
c.ClientIP())
|
|
}
|
|
if svc.Device != nil {
|
|
svc.Device.RecordLogin(c.Request.Context(), resp.User.ID,
|
|
clientInfo.DeviceID,
|
|
clientInfo.DeviceName,
|
|
clientInfo.Client,
|
|
c.ClientIP())
|
|
}
|
|
userPayload, _ := svc.Emby.FindUser(c.Request.Context(), resp.User.ID)
|
|
// Emby/Jellyfin 客户端没有 refresh token 机制:它们把这里返回的
|
|
// AccessToken 长期保存并反复使用。若返回 60 分钟的普通 access
|
|
// token,客户端每小时就会掉登录、无法播放、媒体库无法刷新。因此
|
|
// 签发长期令牌(IssueEmbyToken)匹配 Emby 持久化令牌语义。
|
|
accessToken := resp.Tokens.AccessToken
|
|
if longLived, err := svc.Auth.IssueEmbyToken(resp.User); err == nil && longLived != "" {
|
|
accessToken = longLived
|
|
}
|
|
embyRememberCompatSession(c, accessToken)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"AccessToken": accessToken,
|
|
"ServerId": "mebox-001",
|
|
"User": userPayload,
|
|
"SessionInfo": gin.H{
|
|
"Id": resp.User.ID,
|
|
"UserId": resp.User.ID,
|
|
"UserName": resp.User.Username,
|
|
"Client": clientInfo.Client,
|
|
"DeviceId": clientInfo.DeviceID,
|
|
"DeviceName": clientInfo.DeviceName,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
func embyPublicUsersHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// 公开用户列表(Emby Web 客户端登录页拉这个,列出可见用户)。
|
|
users, err := svc.Emby.ListUsers(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, []any{})
|
|
return
|
|
}
|
|
// 公开版本只暴露 Id + Name,不包含 Policy。
|
|
out := make([]map[string]any, 0, len(users))
|
|
for _, u := range users {
|
|
out = append(out, map[string]any{
|
|
"Id": u["Id"],
|
|
"Name": u["Name"],
|
|
"ServerId": u["ServerId"],
|
|
"HasPassword": true,
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, out)
|
|
}
|
|
}
|
|
|
|
func embyListUsersHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
users, err := svc.Emby.ListUsers(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, users)
|
|
}
|
|
}
|
|
|
|
func embyMeHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uid := embyUserID(c)
|
|
if uid == "" {
|
|
embyError(c, http.StatusUnauthorized, "not authenticated")
|
|
return
|
|
}
|
|
u, err := svc.Emby.FindUser(c.Request.Context(), uid)
|
|
if err != nil || u == nil {
|
|
embyError(c, http.StatusNotFound, "user not found")
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, u)
|
|
}
|
|
}
|
|
|
|
func embyGetUserByIDHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
u, err := svc.Emby.FindUser(c.Request.Context(), c.Param("userId"))
|
|
if err == nil && u != nil {
|
|
c.JSON(http.StatusOK, u)
|
|
return
|
|
}
|
|
if authUID := embyUserID(c); authUID != "" && authUID != c.Param("userId") {
|
|
u, err = svc.Emby.FindUser(c.Request.Context(), authUID)
|
|
if err == nil && u != nil {
|
|
c.JSON(http.StatusOK, u)
|
|
return
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, embyFallbackUser(c.Param("userId")))
|
|
}
|
|
}
|
|
|
|
func embyFallbackUser(id string) gin.H {
|
|
if strings.TrimSpace(id) == "" {
|
|
id = "mebox-user"
|
|
}
|
|
return gin.H{
|
|
"Id": id,
|
|
"Name": "MeBox",
|
|
"ServerId": "mebox-001",
|
|
"HasPassword": true,
|
|
"HasConfiguredPassword": true,
|
|
"HasConfiguredEasyPassword": false,
|
|
"EnableAutoLogin": false,
|
|
"Policy": gin.H{
|
|
"IsAdministrator": true,
|
|
"EnableContentDeletion": true,
|
|
"EnableRemoteControlOfOtherUsers": true,
|
|
"EnableSharedDeviceControl": true,
|
|
"EnableRemoteAccess": true,
|
|
"EnableAllDevices": true,
|
|
"EnableAllChannels": true,
|
|
"EnableAllFolders": true,
|
|
},
|
|
}
|
|
}
|