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: "访问密钥已删除"}) }