mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
44ca451cd4
优化
115 lines
3.9 KiB
Go
115 lines
3.9 KiB
Go
// Package service — authentication token issuance helpers.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
"github.com/ShukeBta/MMTL/internal/model"
|
|
)
|
|
|
|
// IssueToken signs a JWT for the given user (60min validity, includes tier).
|
|
func (s *AuthService) IssueToken(u *model.User) (string, error) {
|
|
claims := Claims{
|
|
UserID: u.ID,
|
|
Role: u.Role,
|
|
Tier: u.Tier,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(60 * time.Minute)),
|
|
Issuer: "mmtl",
|
|
Subject: u.ID,
|
|
},
|
|
}
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return t.SignedString([]byte(s.cfg.Secrets.JWTSecret))
|
|
}
|
|
|
|
const (
|
|
ExternalPlaybackTokenPurpose = "external_play"
|
|
ExternalPlaybackTokenMinDuration = 15 * time.Minute
|
|
ExternalPlaybackTokenGraceDuration = 30 * time.Minute
|
|
ExternalPlaybackTokenUnknownDuration = 6 * time.Hour
|
|
ExternalPlaybackTokenMaxDuration = 24 * time.Hour
|
|
)
|
|
|
|
func ExternalPlaybackTokenDurationForMedia(durationSec int) time.Duration {
|
|
if durationSec <= 0 {
|
|
return ExternalPlaybackTokenUnknownDuration
|
|
}
|
|
duration := time.Duration(durationSec)*time.Second + ExternalPlaybackTokenGraceDuration
|
|
if duration < ExternalPlaybackTokenMinDuration {
|
|
return ExternalPlaybackTokenMinDuration
|
|
}
|
|
if duration > ExternalPlaybackTokenMaxDuration {
|
|
return ExternalPlaybackTokenMaxDuration
|
|
}
|
|
return duration
|
|
}
|
|
|
|
// IssueExternalPlaybackToken signs a short-lived, media-scoped JWT for URLs
|
|
// that are handed to third-party players. It must not be accepted as a
|
|
// reusable account/session token for arbitrary media playback.
|
|
func (s *AuthService) IssueExternalPlaybackToken(u *model.User, mediaID string, durationSec int) (string, error) {
|
|
mediaID = strings.TrimSpace(mediaID)
|
|
if mediaID == "" {
|
|
return "", errors.New("media id required")
|
|
}
|
|
claims := Claims{
|
|
UserID: u.ID,
|
|
Role: u.Role,
|
|
Tier: u.Tier,
|
|
Purpose: ExternalPlaybackTokenPurpose,
|
|
MediaID: mediaID,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ExternalPlaybackTokenDurationForMedia(durationSec))),
|
|
Issuer: "mmtl",
|
|
Subject: u.ID,
|
|
},
|
|
}
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return t.SignedString([]byte(s.cfg.Secrets.JWTSecret))
|
|
}
|
|
|
|
// EmbyTokenDuration 是第三方 Emby/Jellyfin 客户端访问令牌的有效期。
|
|
// Emby 协议没有 refresh token 机制——客户端登录一次后把 AccessToken
|
|
// 长期保存并反复使用,直到用户主动登出。若给它们签发 60 分钟的普通
|
|
// access token,客户端每小时就会掉登录、无法播放、媒体库无法刷新。
|
|
// 因此为这些设备签发长期令牌(与 refresh token 一致的 30 天),匹配
|
|
// Emby 持久化令牌的语义。
|
|
const EmbyTokenDuration = 30 * 24 * time.Hour
|
|
|
|
// IssueEmbyToken 为第三方客户端(Emby/Jellyfin 兼容层)签发一个长期
|
|
// JWT。它与普通 access token 使用相同的密钥与 Claims,因此沿用现有的
|
|
// EmbyAuthRequired 校验逻辑,只是有效期更长。
|
|
func (s *AuthService) IssueEmbyToken(u *model.User) (string, error) {
|
|
claims := Claims{
|
|
UserID: u.ID,
|
|
Role: u.Role,
|
|
Tier: u.Tier,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(EmbyTokenDuration)),
|
|
Issuer: "mmtl",
|
|
Subject: u.ID,
|
|
},
|
|
}
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return t.SignedString([]byte(s.cfg.Secrets.JWTSecret))
|
|
}
|
|
|
|
// RefreshTokens 使用刷新令牌获取新的令牌对。
|
|
func (s *AuthService) RefreshTokens(ctx context.Context, refreshToken string) (*TokenPair, error) {
|
|
return s.tokenSvc.Refresh(ctx, refreshToken)
|
|
}
|
|
|
|
// Logout 撤销用户的所有刷新令牌。
|
|
func (s *AuthService) Logout(ctx context.Context, userID string) error {
|
|
return s.tokenSvc.RevokeAll(ctx, userID)
|
|
}
|