Files
2026-07-09 14:16:56 +08:00

317 lines
9.9 KiB
Go

package controller
import (
"crypto/rand"
"fmt"
"math/big"
"net/http"
"time"
"ezops/middleware"
"ezops/model"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
const apiKeyChars = "abcdefghijklmnopqrstuvwxyz0123456789"
const (
defaultAPIKeyMonths = 12
maxAPIKeyMonths = 36
)
// randomAPIKeyPart 生成指定长度的随机密钥片段 // Logan.Li
func randomAPIKeyPart(n int) (string, error) {
out := make([]byte, n)
max := big.NewInt(int64(len(apiKeyChars)))
for i := range out {
idx, err := rand.Int(rand.Reader, max)
if err != nil {
return "", err
}
out[i] = apiKeyChars[idx.Int64()]
}
return string(out), nil
}
func normalizeAPIKeyMonths(months int) int {
if months <= 0 {
return defaultAPIKeyMonths
}
if months > maxAPIKeyMonths {
return maxAPIKeyMonths
}
return months
}
func apiKeyExpiresAfter(months int) time.Time {
return time.Now().AddDate(0, normalizeAPIKeyMonths(months), 0)
}
func parseExpiresAt(dateStr string) (*time.Time, error) {
if dateStr == "" {
return nil, nil
}
t, err := time.ParseInLocation("2006-01-02", dateStr, time.Local)
if err != nil {
return nil, err
}
end := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, time.Local)
return &end, nil
}
func apiKeyStatus(expiresAt *time.Time) string {
if expiresAt != nil && expiresAt.Before(time.Now()) {
return "expired"
}
return "active"
}
func (h *UserHandler) loadAPIKeyUser(userID uint) (model.User, error) {
var user model.User
err := h.db.Select(
"id", "api_access_key_prefix", "api_access_key_id", "api_access_key_description",
"api_access_key_duration_months", "api_access_key_at", "api_access_key_expires_at",
"api_access_key_last_used_at",
).First(&user, userID).Error
return user, err
}
// apiKeyResponse 单条密钥元信息(不含明文)// Logan.Li
func apiKeyResponse(user model.User) gin.H {
hasToken := user.ApiAccessKeyPrefix != ""
expired := false
var daysLeft *int
if user.ApiAccessKeyExpiresAt != nil {
now := time.Now()
if user.ApiAccessKeyExpiresAt.Before(now) {
expired = true
d := 0
daysLeft = &d
} else {
d := int(user.ApiAccessKeyExpiresAt.Sub(now).Hours()/24) + 1
daysLeft = &d
}
}
mask := ""
if user.ApiAccessKeyPrefix != "" {
mask = user.ApiAccessKeyPrefix + ".•••••••••••••••••••••••••••••••••"
}
return gin.H{
"has_token": hasToken,
"key_id": user.ApiAccessKeyID,
"prefix": user.ApiAccessKeyPrefix,
"access_key_mask": mask,
"description": user.ApiAccessKeyDescription,
"duration_months": user.ApiAccessKeyDurationMonths,
"created_at": user.ApiAccessKeyAt,
"expires_at": user.ApiAccessKeyExpiresAt,
"last_used_at": user.ApiAccessKeyLastUsedAt,
"status": apiKeyStatus(user.ApiAccessKeyExpiresAt),
"expired": expired,
"days_left": daysLeft,
"default_months": defaultAPIKeyMonths,
}
}
func (h *UserHandler) ensureAPIKeyMeta(user *model.User) {
if user.ApiAccessKeyPrefix == "" {
return
}
patches := map[string]interface{}{}
if user.ApiAccessKeyID == "" {
if id, err := randomAPIKeyPart(24); err == nil {
patches["api_access_key_id"] = id
user.ApiAccessKeyID = id
}
}
if user.ApiAccessKeyExpiresAt == nil {
exp := apiKeyExpiresAfter(defaultAPIKeyMonths)
patches["api_access_key_expires_at"] = exp
user.ApiAccessKeyExpiresAt = &exp
}
if user.ApiAccessKeyDurationMonths == 0 {
patches["api_access_key_duration_months"] = defaultAPIKeyMonths
user.ApiAccessKeyDurationMonths = defaultAPIKeyMonths
}
if len(patches) > 0 {
_ = h.db.Model(&model.User{}).Where("id = ?", user.ID).Updates(patches).Error
}
}
// GetApiAccessKey 获取当前用户 API 密钥元信息 // Logan.Li
func (h *UserHandler) GetApiAccessKey(c *gin.Context) {
userID := middleware.GetUserID(c)
user, err := h.loadAPIKeyUser(userID)
if err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "用户不存在"})
return
}
h.ensureAPIKeyMeta(&user)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: apiKeyResponse(user)})
}
type apiKeyCreateRequest struct {
Description string `json:"description"`
DurationMonths int `json:"duration_months"`
ExpiresAt string `json:"expires_at"`
}
type apiKeyUpdateRequest struct {
Description string `json:"description"`
ExpiresAt string `json:"expires_at"`
}
// RegenerateApiAccessKey 创建或重新生成唯一 API 密钥 // Logan.Li
func (h *UserHandler) RegenerateApiAccessKey(c *gin.Context) {
userID := middleware.GetUserID(c)
var req apiKeyCreateRequest
_ = c.ShouldBindJSON(&req)
months := normalizeAPIKeyMonths(req.DurationMonths)
var expires *time.Time
if req.ExpiresAt != "" {
parsed, err := parseExpiresAt(req.ExpiresAt)
if err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "截止时间格式无效,请使用 YYYY-MM-DD"})
return
}
expires = parsed
} else {
exp := apiKeyExpiresAfter(months)
expires = &exp
}
keyID, err := randomAPIKeyPart(24)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "生成密钥失败"})
return
}
prefix, err := randomAPIKeyPart(12)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "生成密钥失败"})
return
}
secret, err := randomAPIKeyPart(32)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "生成密钥失败"})
return
}
full := fmt.Sprintf("%s.%s", prefix, secret)
hash, err := bcrypt.GenerateFromPassword([]byte(full), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "密钥加密失败"})
return
}
now := time.Now()
updates := map[string]interface{}{
"api_access_key_hash": string(hash),
"api_access_key_prefix": prefix,
"api_access_key_id": keyID,
"api_access_key_description": req.Description,
"api_access_key_duration_months": months,
"api_access_key_at": now,
"api_access_key_expires_at": expires,
"api_access_key_last_used_at": nil,
}
if err := h.db.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "保存密钥失败"})
return
}
c.JSON(http.StatusOK, model.Response{
Code: 200, Message: "访问密钥已生成,请立即保存(仅显示一次)",
Data: gin.H{
"token": full,
"prefix": prefix,
"key_id": keyID,
"description": req.Description,
"created_at": now,
"expires_at": expires,
"duration_months": months,
},
})
}
// UpdateApiAccessKey 编辑描述或截止时间(不更换密钥明文)// Logan.Li
func (h *UserHandler) UpdateApiAccessKey(c *gin.Context) {
userID := middleware.GetUserID(c)
var req apiKeyUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
return
}
user, err := h.loadAPIKeyUser(userID)
if err != nil || user.ApiAccessKeyPrefix == "" {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "尚未创建 API 密钥"})
return
}
updates := map[string]interface{}{}
if req.Description != "" || c.Request.ContentLength > 0 {
updates["api_access_key_description"] = req.Description
}
if req.ExpiresAt != "" {
parsed, err := parseExpiresAt(req.ExpiresAt)
if err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "截止时间格式无效"})
return
}
updates["api_access_key_expires_at"] = parsed
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "没有可更新的字段"})
return
}
if err := h.db.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败"})
return
}
user, _ = h.loadAPIKeyUser(userID)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已更新", Data: apiKeyResponse(user)})
}
// ExtendApiAccessKey 延长有效期(兼容旧接口)// Logan.Li
func (h *UserHandler) ExtendApiAccessKey(c *gin.Context) {
userID := middleware.GetUserID(c)
var req struct {
DurationMonths int `json:"duration_months"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
return
}
user, err := h.loadAPIKeyUser(userID)
if err != nil || user.ApiAccessKeyPrefix == "" {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "尚未创建 API 密钥"})
return
}
months := normalizeAPIKeyMonths(req.DurationMonths)
start := time.Now()
if user.ApiAccessKeyExpiresAt != nil && user.ApiAccessKeyExpiresAt.After(start) {
start = *user.ApiAccessKeyExpiresAt
}
expires := start.AddDate(0, months, 0)
if err := h.db.Model(&model.User{}).Where("id = ?", userID).Update("api_access_key_expires_at", expires).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "续期失败"})
return
}
c.JSON(http.StatusOK, model.Response{
Code: 200, Message: "密钥有效期已延长",
Data: gin.H{"expires_at": expires, "duration_months": months},
})
}
// RevokeApiAccessKey 删除 API 访问密钥 // Logan.Li
func (h *UserHandler) RevokeApiAccessKey(c *gin.Context) {
userID := middleware.GetUserID(c)
if err := h.db.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"api_access_key_hash": "", "api_access_key_prefix": "", "api_access_key_id": "",
"api_access_key_description": "", "api_access_key_duration_months": 0,
"api_access_key_at": nil, "api_access_key_expires_at": nil, "api_access_key_last_used_at": nil,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "访问密钥已删除"})
}