feat: 新增日志中心、功能优化与代码重构
- 新增 log_datasource.go / log_proxy.go / user_api_token.go 后端控制器 - 新增 log_datasource.go model - 新增 monitor/log-center.ts API 模块 - 新增 LogCenter 前端组件及页面 - 重构 deployment 相关组件 (HPDA/UpdateImageModal/bundle-yaml 等) - 新增 ResetPasswordDialog / code-snippet-block / copy-button 等 UI 组件 - 重构多个 k8s 相关组件 (TopologyView/ResourceVisualEditor/PodFileBrowser 等) - 更新多个 API 和工具函数 (auth.ts/monitor/index.ts/request.ts 等) - 更新静态资源 (index.html + 新 JS/CSS bundle) - 删除废弃静态资源文件
This commit is contained in:
1 parent
5ca0e6d83a
commit
8d46e8f352
197 files changed
+5014
-815
No files matched your search
@@ -0,0 +1,314 @@
|
||||
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: "访问密钥已删除"})
|
||||
}
|
||||
Reference in new issue
Block a user