Q4 修复了 controller/ 层 8 处 DB 写入的 _ = 静默忽略,但后续合并的 RDP
远程桌面、API Token、AIOps 告警分析等模块引入了新的 _ = DB 写入忽略。
本次将 controller/middleware 层所有 DB 写入操作(Create/Update/Updates)
的 _ = 静默忽略改为带 log.Printf 错误日志(不阻断业务流程)。
修改文件 (6):
- middleware/auth.go: API 密钥最后使用时间更新
- controller/terminal_session.go: 终端回放审计日志 + 修复 log 变量遮蔽
- controller/user_api_token.go: API 密钥默认值补全
- controller/rdp.go: RDP 会话结束状态 + 命令记录
- controller/rdp_session.go: 会话终止 + 回放审计 + 修复 log 变量遮蔽
- controller/aiops_alert_analysis.go: AI 告警分析结果保存
修复 2 处 Go 变量名遮蔽陷阱(log := model.AuditLog{} 遮蔽 "log" 包 → auditLog)
保留 4 处可接受忽略(1 DB 读取 + 3 WebSocket 帧写入)
验证: go build=0, 17/17 集成测试通过, Docker healthy
317 lines
10 KiB
Go
317 lines
10 KiB
Go
package controller
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"log"
|
|
"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 {
|
|
if err := h.db.Model(&model.User{}).Where("id = ?", user.ID).Updates(patches).Error; err != nil {
|
|
log.Printf("[UserApiKey] 补全 API 密钥默认值失败 (user_id=%d): %v", user.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: "访问密钥已删除"})
|
|
} |