2025-11-28 23:30:49 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
2025-12-02 19:43:39 +08:00
|
|
|
|
"carrotskin/internal/container"
|
2025-11-28 23:30:49 +08:00
|
|
|
|
"carrotskin/internal/model"
|
|
|
|
|
|
"carrotskin/pkg/utils"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 常量定义
|
|
|
|
|
|
const (
|
|
|
|
|
|
ErrInternalServer = "服务器内部错误"
|
|
|
|
|
|
// 错误类型
|
|
|
|
|
|
ErrInvalidEmailFormat = "邮箱格式不正确"
|
|
|
|
|
|
ErrInvalidPassword = "密码必须至少包含8个字符,只能包含字母、数字和特殊字符"
|
|
|
|
|
|
ErrWrongPassword = "密码错误"
|
|
|
|
|
|
ErrUserNotMatch = "用户不匹配"
|
|
|
|
|
|
|
|
|
|
|
|
// 错误消息
|
|
|
|
|
|
ErrInvalidRequest = "请求格式无效"
|
|
|
|
|
|
ErrJoinServerFailed = "加入服务器失败"
|
|
|
|
|
|
ErrServerIDRequired = "服务器ID不能为空"
|
|
|
|
|
|
ErrUsernameRequired = "用户名不能为空"
|
|
|
|
|
|
ErrSessionVerifyFailed = "会话验证失败"
|
|
|
|
|
|
ErrProfileNotFound = "未找到用户配置文件"
|
|
|
|
|
|
ErrInvalidParams = "无效的请求参数"
|
|
|
|
|
|
ErrEmptyUserID = "用户ID为空"
|
|
|
|
|
|
ErrUnauthorized = "无权操作此配置文件"
|
|
|
|
|
|
ErrGetProfileService = "获取配置文件服务失败"
|
|
|
|
|
|
|
|
|
|
|
|
// 成功信息
|
|
|
|
|
|
SuccessProfileCreated = "创建成功"
|
|
|
|
|
|
MsgRegisterSuccess = "注册成功"
|
|
|
|
|
|
|
|
|
|
|
|
// 错误消息
|
|
|
|
|
|
ErrGetProfile = "获取配置文件失败"
|
|
|
|
|
|
ErrGetTextureService = "获取材质服务失败"
|
|
|
|
|
|
ErrInvalidContentType = "无效的请求内容类型"
|
|
|
|
|
|
ErrParseMultipartForm = "解析多部分表单失败"
|
|
|
|
|
|
ErrGetFileFromForm = "从表单获取文件失败"
|
|
|
|
|
|
ErrInvalidFileType = "无效的文件类型,仅支持PNG图片"
|
|
|
|
|
|
ErrSaveTexture = "保存材质失败"
|
|
|
|
|
|
ErrSetTexture = "设置材质失败"
|
|
|
|
|
|
ErrGetTexture = "获取材质失败"
|
|
|
|
|
|
|
|
|
|
|
|
// 内存限制
|
|
|
|
|
|
MaxMultipartMemory = 32 << 20 // 32 MB
|
|
|
|
|
|
|
|
|
|
|
|
// 材质类型
|
|
|
|
|
|
TextureTypeSkin = "SKIN"
|
|
|
|
|
|
TextureTypeCape = "CAPE"
|
|
|
|
|
|
|
|
|
|
|
|
// 内容类型
|
|
|
|
|
|
ContentTypePNG = "image/png"
|
|
|
|
|
|
ContentTypeMultipart = "multipart/form-data"
|
|
|
|
|
|
|
|
|
|
|
|
// 表单参数
|
|
|
|
|
|
FormKeyModel = "model"
|
|
|
|
|
|
FormKeyFile = "file"
|
|
|
|
|
|
|
|
|
|
|
|
// 元数据键
|
|
|
|
|
|
MetaKeyModel = "model"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 正则表达式
|
|
|
|
|
|
var (
|
|
|
|
|
|
// 邮箱正则表达式
|
|
|
|
|
|
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
|
|
|
|
|
|
|
|
|
|
|
// 密码强度正则表达式(最少8位,只允许字母、数字和特定特殊字符)
|
|
|
|
|
|
passwordRegex = regexp.MustCompile(`^[a-zA-Z0-9!@#$%^&*]{8,}$`)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 请求结构体
|
|
|
|
|
|
type (
|
|
|
|
|
|
// AuthenticateRequest 认证请求
|
|
|
|
|
|
AuthenticateRequest struct {
|
|
|
|
|
|
Agent map[string]interface{} `json:"agent"`
|
|
|
|
|
|
ClientToken string `json:"clientToken"`
|
|
|
|
|
|
Identifier string `json:"username" binding:"required"`
|
|
|
|
|
|
Password string `json:"password" binding:"required"`
|
|
|
|
|
|
RequestUser bool `json:"requestUser"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ValidTokenRequest 验证令牌请求
|
|
|
|
|
|
ValidTokenRequest struct {
|
|
|
|
|
|
AccessToken string `json:"accessToken" binding:"required"`
|
|
|
|
|
|
ClientToken string `json:"clientToken"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RefreshRequest 刷新令牌请求
|
|
|
|
|
|
RefreshRequest struct {
|
|
|
|
|
|
AccessToken string `json:"accessToken" binding:"required"`
|
|
|
|
|
|
ClientToken string `json:"clientToken"`
|
|
|
|
|
|
RequestUser bool `json:"requestUser"`
|
|
|
|
|
|
SelectedProfile map[string]interface{} `json:"selectedProfile"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SignOutRequest 登出请求
|
|
|
|
|
|
SignOutRequest struct {
|
|
|
|
|
|
Email string `json:"username" binding:"required"`
|
|
|
|
|
|
Password string `json:"password" binding:"required"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// JoinServerRequest 加入服务器请求
|
2025-11-28 23:30:49 +08:00
|
|
|
|
JoinServerRequest struct {
|
|
|
|
|
|
ServerID string `json:"serverId" binding:"required"`
|
|
|
|
|
|
AccessToken string `json:"accessToken" binding:"required"`
|
|
|
|
|
|
SelectedProfile string `json:"selectedProfile" binding:"required"`
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 响应结构体
|
|
|
|
|
|
type (
|
|
|
|
|
|
// AuthenticateResponse 认证响应
|
|
|
|
|
|
AuthenticateResponse struct {
|
|
|
|
|
|
AccessToken string `json:"accessToken"`
|
|
|
|
|
|
ClientToken string `json:"clientToken"`
|
|
|
|
|
|
SelectedProfile map[string]interface{} `json:"selectedProfile,omitempty"`
|
|
|
|
|
|
AvailableProfiles []map[string]interface{} `json:"availableProfiles"`
|
|
|
|
|
|
User map[string]interface{} `json:"user,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RefreshResponse 刷新令牌响应
|
|
|
|
|
|
RefreshResponse struct {
|
|
|
|
|
|
AccessToken string `json:"accessToken"`
|
|
|
|
|
|
ClientToken string `json:"clientToken"`
|
|
|
|
|
|
SelectedProfile map[string]interface{} `json:"selectedProfile,omitempty"`
|
|
|
|
|
|
User map[string]interface{} `json:"user,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// APIResponse API响应
|
2025-11-28 23:30:49 +08:00
|
|
|
|
type APIResponse struct {
|
|
|
|
|
|
Status int `json:"status"`
|
|
|
|
|
|
Data interface{} `json:"data"`
|
|
|
|
|
|
Error interface{} `json:"error"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// standardResponse 生成标准响应
|
|
|
|
|
|
func standardResponse(c *gin.Context, status int, data interface{}, err interface{}) {
|
|
|
|
|
|
c.JSON(status, APIResponse{
|
|
|
|
|
|
Status: status,
|
|
|
|
|
|
Data: data,
|
|
|
|
|
|
Error: err,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// YggdrasilHandler Yggdrasil API处理器
|
|
|
|
|
|
type YggdrasilHandler struct {
|
|
|
|
|
|
container *container.Container
|
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewYggdrasilHandler 创建YggdrasilHandler实例
|
|
|
|
|
|
func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler {
|
|
|
|
|
|
return &YggdrasilHandler{
|
|
|
|
|
|
container: c,
|
|
|
|
|
|
logger: c.Logger,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// Authenticate 用户认证
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil认证
|
|
|
|
|
|
// @Description Yggdrasil协议: 用户登录认证
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param request body AuthenticateRequest true "认证请求"
|
|
|
|
|
|
// @Success 200 {object} AuthenticateResponse
|
|
|
|
|
|
// @Failure 403 {object} map[string]string "认证失败"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/authserver/authenticate [post]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
rawData, err := io.ReadAll(c.Request.Body)
|
|
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("读取请求体失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(rawData))
|
|
|
|
|
|
|
|
|
|
|
|
var request AuthenticateRequest
|
|
|
|
|
|
if err = c.ShouldBindJSON(&request); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("解析认证请求失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var userId int64
|
|
|
|
|
|
var profile *model.Profile
|
|
|
|
|
|
var UUID string
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if emailRegex.MatchString(request.Identifier) {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
userId, err = h.container.YggdrasilService.GetUserIDByEmail(c.Request.Context(), request.Identifier)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
} else {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
profile, err = h.container.ProfileRepo.FindByName(c.Request.Context(), request.Identifier)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
userId = profile.UserID
|
|
|
|
|
|
UUID = profile.UUID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("认证失败: 用户不存在", zap.String("identifier", request.Identifier), zap.Error(err))
|
|
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "用户不存在"})
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if err := h.container.YggdrasilService.VerifyPassword(c.Request.Context(), request.Password, userId); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("认证失败: 密码错误", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"error": ErrWrongPassword})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
selectedProfile, availableProfiles, accessToken, clientToken, err := h.container.TokenService.Create(c.Request.Context(), userId, UUID, request.ClientToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("生成令牌失败", zap.Error(err), zap.Int64("userId", userId))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
user, err := h.container.UserService.GetByID(c.Request.Context(), userId)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("获取用户信息失败", zap.Error(err), zap.Int64("userId", userId))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
availableProfilesData := make([]map[string]interface{}, 0, len(availableProfiles))
|
2025-12-02 19:43:39 +08:00
|
|
|
|
for _, p := range availableProfiles {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
availableProfilesData = append(availableProfilesData, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *p))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
response := AuthenticateResponse{
|
|
|
|
|
|
AccessToken: accessToken,
|
|
|
|
|
|
ClientToken: clientToken,
|
|
|
|
|
|
AvailableProfiles: availableProfilesData,
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if selectedProfile != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
response.SelectedProfile = h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *selectedProfile)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
|
|
|
|
|
if request.RequestUser && user != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
response.User = h.container.YggdrasilService.SerializeUser(c.Request.Context(), user, UUID)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("用户认证成功", zap.Int64("userId", userId))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ValidToken 验证令牌
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil验证令牌
|
|
|
|
|
|
// @Description Yggdrasil协议: 验证AccessToken是否有效
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param request body ValidTokenRequest true "验证请求"
|
|
|
|
|
|
// @Success 204 "令牌有效"
|
|
|
|
|
|
// @Failure 403 {object} map[string]bool "令牌无效"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/authserver/validate [post]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
var request ValidTokenRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("解析验证令牌请求失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if h.container.TokenService.Validate(c.Request.Context(), request.AccessToken, request.ClientToken) {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("令牌验证成功", zap.String("accessToken", request.AccessToken))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusNoContent, gin.H{"valid": true})
|
|
|
|
|
|
} else {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("令牌验证失败", zap.String("accessToken", request.AccessToken))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"valid": false})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RefreshToken 刷新令牌
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil刷新令牌
|
|
|
|
|
|
// @Description Yggdrasil协议: 刷新AccessToken
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param request body RefreshRequest true "刷新请求"
|
|
|
|
|
|
// @Success 200 {object} RefreshResponse
|
|
|
|
|
|
// @Failure 400 {object} map[string]string "刷新失败"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/authserver/refresh [post]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
var request RefreshRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("解析刷新令牌请求失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
UUID, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), request.AccessToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("刷新令牌失败: 无效的访问令牌", zap.String("token", request.AccessToken), zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
userID, _ := h.container.TokenService.GetUserIDByAccessToken(c.Request.Context(), request.AccessToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
UUID = utils.FormatUUID(UUID)
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), UUID)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("刷新令牌失败: 无法获取用户信息", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var profileData map[string]interface{}
|
|
|
|
|
|
var userData map[string]interface{}
|
|
|
|
|
|
var profileID string
|
|
|
|
|
|
|
|
|
|
|
|
if request.SelectedProfile != nil {
|
|
|
|
|
|
profileIDValue, ok := request.SelectedProfile["id"]
|
|
|
|
|
|
if !ok {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("刷新令牌失败: 缺少配置文件ID", zap.Int64("userId", userID))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少配置文件ID"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
profileID, ok = profileIDValue.(string)
|
|
|
|
|
|
if !ok {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("刷新令牌失败: 配置文件ID类型错误", zap.Int64("userId", userID))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "配置文件ID必须是字符串"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
profileID = utils.FormatUUID(profileID)
|
|
|
|
|
|
|
|
|
|
|
|
if profile.UserID != userID {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("刷新令牌失败: 用户不匹配",
|
|
|
|
|
|
zap.Int64("userId", userID),
|
|
|
|
|
|
zap.Int64("profileUserId", profile.UserID),
|
|
|
|
|
|
)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": ErrUserNotMatch})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
profileData = h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
user, _ := h.container.UserService.GetByID(c.Request.Context(), userID)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if request.RequestUser && user != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
userData = h.container.YggdrasilService.SerializeUser(c.Request.Context(), user, UUID)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
newAccessToken, newClientToken, err := h.container.TokenService.Refresh(c.Request.Context(),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
request.AccessToken,
|
|
|
|
|
|
request.ClientToken,
|
|
|
|
|
|
profileID,
|
|
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("刷新令牌失败", zap.Error(err), zap.Int64("userId", userID))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("刷新令牌成功", zap.Int64("userId", userID))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusOK, RefreshResponse{
|
|
|
|
|
|
AccessToken: newAccessToken,
|
|
|
|
|
|
ClientToken: newClientToken,
|
|
|
|
|
|
SelectedProfile: profileData,
|
|
|
|
|
|
User: userData,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// InvalidToken 使令牌失效
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil注销令牌
|
|
|
|
|
|
// @Description Yggdrasil协议: 使AccessToken失效
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param request body ValidTokenRequest true "失效请求"
|
|
|
|
|
|
// @Success 204 "操作成功"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/authserver/invalidate [post]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
var request ValidTokenRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("解析使令牌失效请求失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
h.container.TokenService.Invalidate(c.Request.Context(), request.AccessToken)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("令牌已失效", zap.String("token", request.AccessToken))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusNoContent, gin.H{})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SignOut 用户登出
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil登出
|
|
|
|
|
|
// @Description Yggdrasil协议: 用户登出,使所有令牌失效
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param request body SignOutRequest true "登出请求"
|
|
|
|
|
|
// @Success 204 "操作成功"
|
|
|
|
|
|
// @Failure 400 {object} map[string]string "参数错误"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/authserver/signout [post]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) SignOut(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
var request SignOutRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("解析登出请求失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !emailRegex.MatchString(request.Email) {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("登出失败: 邮箱格式不正确", zap.String("email", request.Email))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidEmailFormat})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
user, err := h.container.UserService.GetByEmail(c.Request.Context(), request.Email)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if err != nil || user == nil {
|
|
|
|
|
|
h.logger.Warn("登出失败: 用户不存在", zap.String("email", request.Email), zap.Error(err))
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "用户不存在"})
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if err := h.container.YggdrasilService.VerifyPassword(c.Request.Context(), request.Password, user.ID); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("登出失败: 密码错误", zap.Int64("userId", user.ID))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
h.container.TokenService.InvalidateUserTokens(c.Request.Context(), user.ID)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("用户登出成功", zap.Int64("userId", user.ID))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusNoContent, gin.H{"valid": true})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// GetProfileByUUID 根据UUID获取档案
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil获取档案
|
|
|
|
|
|
// @Description Yggdrasil协议: 根据UUID获取用户档案信息
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param uuid path string true "用户UUID"
|
|
|
|
|
|
// @Success 200 {object} map[string]interface{} "档案信息"
|
|
|
|
|
|
// @Failure 500 {object} APIResponse "服务器错误"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/profile/{uuid} [get]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
uuid := utils.FormatUUID(c.Param("uuid"))
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("获取配置文件请求", zap.String("uuid", uuid))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), uuid)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("获取配置文件失败", zap.Error(err), zap.String("uuid", uuid))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusInternalServerError, nil, err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("成功获取配置文件", zap.String("uuid", uuid), zap.String("name", profile.Name))
|
2025-12-02 22:52:33 +08:00
|
|
|
|
c.JSON(http.StatusOK, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// JoinServer 加入服务器
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil加入服务器
|
|
|
|
|
|
// @Description Yggdrasil协议: 客户端加入服务器
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param request body JoinServerRequest true "加入请求"
|
|
|
|
|
|
// @Success 204 "加入成功"
|
|
|
|
|
|
// @Failure 400 {object} APIResponse "参数错误"
|
|
|
|
|
|
// @Failure 500 {object} APIResponse "服务器错误"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/join [post]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
var request JoinServerRequest
|
|
|
|
|
|
clientIP := c.ClientIP()
|
|
|
|
|
|
|
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("解析加入服务器请求失败", zap.Error(err), zap.String("ip", clientIP))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusBadRequest, nil, ErrInvalidRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("收到加入服务器请求",
|
|
|
|
|
|
zap.String("serverId", request.ServerID),
|
|
|
|
|
|
zap.String("userUUID", request.SelectedProfile),
|
|
|
|
|
|
zap.String("ip", clientIP),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if err := h.container.YggdrasilService.JoinServer(c.Request.Context(), request.ServerID, request.AccessToken, request.SelectedProfile, clientIP); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("加入服务器失败",
|
2025-11-28 23:30:49 +08:00
|
|
|
|
zap.Error(err),
|
2025-12-02 19:43:39 +08:00
|
|
|
|
zap.String("serverId", request.ServerID),
|
|
|
|
|
|
zap.String("userUUID", request.SelectedProfile),
|
|
|
|
|
|
zap.String("ip", clientIP),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
standardResponse(c, http.StatusInternalServerError, nil, ErrJoinServerFailed)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("加入服务器成功",
|
|
|
|
|
|
zap.String("serverId", request.ServerID),
|
|
|
|
|
|
zap.String("userUUID", request.SelectedProfile),
|
|
|
|
|
|
zap.String("ip", clientIP),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
c.Status(http.StatusNoContent)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// HasJoinedServer 验证玩家是否已加入服务器
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil验证加入
|
|
|
|
|
|
// @Description Yggdrasil协议: 服务端验证客户端是否已加入
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param username query string true "用户名"
|
|
|
|
|
|
// @Param serverId query string true "服务器ID"
|
|
|
|
|
|
// @Param ip query string false "客户端IP"
|
|
|
|
|
|
// @Success 200 {object} map[string]interface{} "验证成功,返回档案"
|
|
|
|
|
|
// @Failure 204 "验证失败"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/hasJoined [get]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
clientIP, _ := c.GetQuery("ip")
|
|
|
|
|
|
|
|
|
|
|
|
serverID, exists := c.GetQuery("serverId")
|
|
|
|
|
|
if !exists || serverID == "" {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("缺少服务器ID参数", zap.String("ip", clientIP))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusNoContent, nil, ErrServerIDRequired)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
username, exists := c.GetQuery("username")
|
|
|
|
|
|
if !exists || username == "" {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("缺少用户名参数", zap.String("serverId", serverID), zap.String("ip", clientIP))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusNoContent, nil, ErrUsernameRequired)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("收到会话验证请求",
|
|
|
|
|
|
zap.String("serverId", serverID),
|
|
|
|
|
|
zap.String("username", username),
|
|
|
|
|
|
zap.String("ip", clientIP),
|
|
|
|
|
|
)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if err := h.container.YggdrasilService.HasJoinedServer(c.Request.Context(), serverID, username, clientIP); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Warn("会话验证失败",
|
2025-11-28 23:30:49 +08:00
|
|
|
|
zap.Error(err),
|
2025-12-02 19:43:39 +08:00
|
|
|
|
zap.String("serverId", serverID),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
zap.String("username", username),
|
2025-12-02 19:43:39 +08:00
|
|
|
|
zap.String("ip", clientIP),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
standardResponse(c, http.StatusNoContent, nil, ErrSessionVerifyFailed)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), username)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("获取用户配置文件失败", zap.Error(err), zap.String("username", username))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusNoContent, nil, ErrProfileNotFound)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("会话验证成功",
|
|
|
|
|
|
zap.String("serverId", serverID),
|
|
|
|
|
|
zap.String("username", username),
|
|
|
|
|
|
zap.String("uuid", profile.UUID),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
2025-12-02 22:52:33 +08:00
|
|
|
|
c.JSON(200, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// GetProfilesByName 批量获取配置文件
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil批量获取档案
|
|
|
|
|
|
// @Description Yggdrasil协议: 根据名称批量获取用户档案
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param request body []string true "用户名列表"
|
|
|
|
|
|
// @Success 200 {array} model.Profile "档案列表"
|
|
|
|
|
|
// @Failure 400 {object} APIResponse "参数错误"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/api/profiles/minecraft [post]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
var names []string
|
|
|
|
|
|
|
|
|
|
|
|
if err := c.ShouldBindJSON(&names); err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("解析名称数组请求失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusBadRequest, nil, ErrInvalidParams)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("接收到批量获取配置文件请求", zap.Int("count", len(names)))
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
profiles, err := h.container.ProfileService.GetByNames(c.Request.Context(), names)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("获取配置文件失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("成功获取配置文件", zap.Int("requested", len(names)), zap.Int("returned", len(profiles)))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusOK, profiles)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// GetMetaData 获取Yggdrasil元数据
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil元数据
|
|
|
|
|
|
// @Description Yggdrasil协议: 获取服务器元数据
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Success 200 {object} map[string]interface{} "元数据"
|
|
|
|
|
|
// @Failure 500 {object} APIResponse "服务器错误"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil [get]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
meta := gin.H{
|
|
|
|
|
|
"implementationName": "CellAuth",
|
|
|
|
|
|
"implementationVersion": "0.0.1",
|
|
|
|
|
|
"serverName": "LittleLan's Yggdrasil Server Implementation.",
|
|
|
|
|
|
"links": gin.H{
|
|
|
|
|
|
"homepage": "https://skin.littlelan.cn",
|
|
|
|
|
|
"register": "https://skin.littlelan.cn/auth",
|
|
|
|
|
|
},
|
|
|
|
|
|
"feature.non_email_login": true,
|
|
|
|
|
|
"feature.enable_profile_key": true,
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
skinDomains := []string{".hitwh.games", ".littlelan.cn"}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
signature, err := h.container.YggdrasilService.GetPublicKey(c.Request.Context())
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("获取公钥失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("提供元数据")
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"meta": meta,
|
2025-11-28 23:30:49 +08:00
|
|
|
|
"skinDomains": skinDomains,
|
2025-12-02 19:43:39 +08:00
|
|
|
|
"signaturePublickey": signature,
|
|
|
|
|
|
})
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// GetPlayerCertificates 获取玩家证书
|
2025-12-26 01:15:17 +08:00
|
|
|
|
// @Summary Yggdrasil获取证书
|
|
|
|
|
|
// @Description Yggdrasil协议: 获取玩家证书
|
|
|
|
|
|
// @Tags Yggdrasil
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param Authorization header string true "Bearer {token}"
|
|
|
|
|
|
// @Success 200 {object} map[string]interface{} "证书信息"
|
|
|
|
|
|
// @Failure 401 {object} map[string]string "未授权"
|
|
|
|
|
|
// @Failure 500 {object} APIResponse "服务器错误"
|
|
|
|
|
|
// @Router /api/v1/yggdrasil/minecraftservices/player/certificates [post]
|
2025-12-02 19:43:39 +08:00
|
|
|
|
func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
authHeader := c.GetHeader("Authorization")
|
|
|
|
|
|
if authHeader == "" {
|
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header not provided"})
|
|
|
|
|
|
c.Abort()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bearerPrefix := "Bearer "
|
|
|
|
|
|
if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
|
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
|
|
|
|
|
|
c.Abort()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
tokenID := authHeader[len(bearerPrefix):]
|
|
|
|
|
|
if tokenID == "" {
|
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
|
|
|
|
|
|
c.Abort()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
uuid, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), tokenID)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if uuid == "" {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("获取玩家UUID失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uuid = utils.FormatUUID(uuid)
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
certificate, err := h.container.YggdrasilService.GeneratePlayerCertificate(c.Request.Context(), uuid)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Error("生成玩家证书失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
h.logger.Info("成功生成玩家证书")
|
2025-11-28 23:30:49 +08:00
|
|
|
|
c.JSON(http.StatusOK, certificate)
|
|
|
|
|
|
}
|