Files
truewhile 28aa466107 feat: 支持临时登录密码并完善 Emby 媒体详情兼容
- 新增 6 位临时密码生成与验证接口,支持 TV 及客户端快速登录
- 媒体及剧集详情补充 People 演职员信息解析与返回
- 优化剧集背景图与海报标签继承机制及播放时长兜底逻辑
- 增加临时密码登录相关的单元测试
2026-09-06 22:58:41 +08:00

145 lines
4.2 KiB
Go

// Package handler — auth-related HTTP endpoints.
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/service"
)
type loginReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type registerReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
func loginHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
var req loginReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
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
}
if svc.Sessions != nil {
svc.Sessions.RecordLogin(c.Request.Context(), resp.User.ID, resp.User.Username, "", "Web", "Web", c.ClientIP())
}
if resp.Tokens != nil {
setAccessTokenCookie(c, resp.Tokens.AccessToken, int(resp.Tokens.ExpiresIn))
}
c.JSON(http.StatusOK, gin.H{
"user": resp.User,
"tokens": resp.Tokens,
})
svc.Audit.RecordBestEffort(resp.User.ID, "auth.login", resp.User.Username, c.ClientIP(), "")
}
}
func registerHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
var req registerReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
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"})
return
}
if errors.Is(err, service.ErrUserLimitReached) {
maxUsers, loadErr := service.LoadMaxUsers(c.Request.Context(), svc.Repo)
if loadErr != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": loadErr.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "user limit reached", "max_users": maxUsers})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if tokens != nil {
setAccessTokenCookie(c, tokens.AccessToken, int(tokens.ExpiresIn))
}
c.JSON(http.StatusCreated, gin.H{
"user": u,
"tokens": tokens,
})
}
}
func meHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
uid, _ := c.Get(middleware.CtxUserID)
u, err := svc.Repo.User.FindByID(c.Request.Context(), uid.(string))
if err != nil || u == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
u.PopulateComputedFields()
c.JSON(http.StatusOK, u)
}
}
type changePwdReq struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
func changePasswordHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
var req changePwdReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
uid, _ := c.Get(middleware.CtxUserID)
if err := svc.Auth.ChangePassword(c.Request.Context(), uid.(string), req.OldPassword, req.NewPassword); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
}
func temporaryPasswordHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
uid, ok := c.Get(middleware.CtxUserID)
if !ok || uid == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
code, expireSec, err := svc.Auth.CreateTemporaryPassword(c.Request.Context(), uid.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"expires_in": expireSec,
})
}
}