mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
33a1c32cf8
Reorganize internal packages into platform/infra/shared layers and update imports, docs, and seed-count tests to match current system configs.
194 lines
5.6 KiB
Go
194 lines
5.6 KiB
Go
// Copyright 2025 linux.do
|
||
// Copyright 2026 Arctel.net
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
// Package user 提供用户认证与帐户管理功能
|
||
package user
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||
"github.com/Rain-kl/Wavelet/internal/model"
|
||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"github.com/Rain-kl/Wavelet/internal/shared/response"
|
||
)
|
||
|
||
type createTokenRequest struct {
|
||
Name string `json:"name"`
|
||
IsAdmin bool `json:"is_admin"`
|
||
}
|
||
|
||
type tokenResponse struct {
|
||
Token string `json:"token"`
|
||
Record model.AccessToken `json:"record"`
|
||
}
|
||
|
||
// ListAccessTokens 获取当前用户的 AccessToken 列表
|
||
// @Summary 获取当前用户的 AccessToken 列表
|
||
// @Description 返回当前登录用户的所有 active access tokens(脱敏后)
|
||
// @Tags user
|
||
// @Produce json
|
||
// @Security SessionCookie
|
||
// @Success 200 {object} response.Any{data=[]model.AccessToken} "令牌列表"
|
||
// @Failure 401 {object} response.Any "未登录"
|
||
// @Router /api/v1/user/access-tokens [get]
|
||
// ListAccessTokens 获取当前用户的 AccessToken 列表
|
||
func ListAccessTokens(c *gin.Context) {
|
||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||
ctx := c.Request.Context()
|
||
|
||
tokens, err := listAccessTokensLogic(ctx, currUser.ID)
|
||
if err != nil {
|
||
response.AbortBadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, response.OK(tokens))
|
||
}
|
||
|
||
// CreateAccessToken 创建一个新的 AccessToken
|
||
// @Summary 创建一个新的 AccessToken
|
||
// @Description 为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。可通过 is_admin 字段赋予令牌管理员权限(仅管理员用户可设置)。
|
||
// @Tags user
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param request body user.createTokenRequest true "令牌名称"
|
||
// @Security SessionCookie
|
||
// @Success 200 {object} response.Any{data=user.tokenResponse} "新建令牌成功"
|
||
// @Failure 400 {object} response.Any "参数错误或超限"
|
||
// @Router /api/v1/user/access-tokens [post]
|
||
func CreateAccessToken(c *gin.Context) {
|
||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||
ctx := c.Request.Context()
|
||
|
||
var req createTokenRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.AbortBadRequest(c, errBindParamsFailed)
|
||
return
|
||
}
|
||
|
||
req.Name = strings.TrimSpace(req.Name)
|
||
if req.Name == "" {
|
||
response.AbortBadRequest(c, errTokenNameRequired)
|
||
return
|
||
}
|
||
|
||
// 只有管理员才能创建具有管理员权限的令牌
|
||
if req.IsAdmin && !currUser.IsAdmin {
|
||
response.AbortBadRequest(c, errAdminTokenRequiresAdmin)
|
||
return
|
||
}
|
||
|
||
// 检查最大限制(基于 ConfigKeyMaxAPIKeysPerUser 配置,默认值为 5)
|
||
maxLimit := 5
|
||
if val, err := repository.GetIntByKey(ctx, model.ConfigKeyMaxAPIKeysPerUser); err == nil {
|
||
maxLimit = val
|
||
}
|
||
|
||
count, err := countAccessTokensLogic(ctx, currUser.ID)
|
||
if err != nil {
|
||
response.AbortBadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
if int(count) >= maxLimit {
|
||
response.AbortBadRequest(c, errAccessTokenLimitReached)
|
||
return
|
||
}
|
||
|
||
// 生成 Token
|
||
tokenStr, err := model.GenerateTokenString()
|
||
if err != nil {
|
||
response.AbortBadRequest(c, errGenerateTokenFailed)
|
||
return
|
||
}
|
||
|
||
tokenHash := model.HashToken(tokenStr)
|
||
maskedToken := model.MaskTokenString(tokenStr)
|
||
|
||
tokenRecord := model.AccessToken{
|
||
UserID: currUser.ID,
|
||
Name: req.Name,
|
||
TokenHash: tokenHash,
|
||
MaskedToken: maskedToken,
|
||
IsAdmin: req.IsAdmin,
|
||
}
|
||
|
||
if err := createAccessTokenLogic(ctx, &tokenRecord); err != nil {
|
||
response.AbortBadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, response.OK(tokenResponse{
|
||
Token: tokenStr,
|
||
Record: tokenRecord,
|
||
}))
|
||
}
|
||
|
||
// DeleteAccessToken 删除一个 AccessToken
|
||
// @Summary 删除一个 AccessToken
|
||
// @Description 撤销并删除一个属于当前用户的 API 访问令牌
|
||
// @Tags user
|
||
// @Produce json
|
||
// @Param id path string true "令牌ID"
|
||
// @Security SessionCookie
|
||
// @Success 200 {object} response.Any{data=string} "删除成功"
|
||
// @Failure 400 {object} response.Any "参数错误"
|
||
// @Router /api/v1/user/access-tokens/{id} [delete]
|
||
func DeleteAccessToken(c *gin.Context) {
|
||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||
ctx := c.Request.Context()
|
||
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
response.AbortBadRequest(c, errInvalidTokenID)
|
||
return
|
||
}
|
||
|
||
if err := deleteAccessTokenLogic(ctx, id, currUser.ID); err != nil {
|
||
response.AbortBadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, response.OK("删除成功"))
|
||
}
|
||
|
||
// RotateAccessToken 轮换一个 AccessToken
|
||
// @Summary 轮换一个 AccessToken
|
||
// @Description 轮换(重新生成)一个属于当前用户的 API 访问令牌的密钥,旧令牌将立即失效
|
||
// @Tags user
|
||
// @Produce json
|
||
// @Param id path string true "令牌ID"
|
||
// @Security SessionCookie
|
||
// @Success 200 {object} response.Any{data=user.tokenResponse} "令牌轮换成功"
|
||
// @Failure 400 {object} response.Any "参数错误"
|
||
// @Router /api/v1/user/access-tokens/{id}/rotate [post]
|
||
func RotateAccessToken(c *gin.Context) {
|
||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||
ctx := c.Request.Context()
|
||
|
||
idStr := c.Param("id")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
response.AbortBadRequest(c, errInvalidTokenID)
|
||
return
|
||
}
|
||
|
||
newTokenStr, tokenRecord, err := rotateAccessTokenLogic(ctx, id, currUser.ID)
|
||
if err != nil {
|
||
response.AbortBadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, response.OK(tokenResponse{
|
||
Token: newTokenStr,
|
||
Record: *tokenRecord,
|
||
}))
|
||
}
|