317 lines
9.9 KiB
Go
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: "访问密钥已删除"})
|
|
}
|