mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
Use the gin framework
This commit is contained in:
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -14,7 +15,9 @@ import (
|
||||
"networkDev/server"
|
||||
"networkDev/utils"
|
||||
"networkDev/utils/logger"
|
||||
"networkDev/web"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
@@ -90,24 +93,46 @@ func getServerPort(cmd *cobra.Command) int {
|
||||
|
||||
// createHTTPServer 创建HTTP服务器
|
||||
func createHTTPServer(addr string) *http.Server {
|
||||
mux := http.NewServeMux()
|
||||
// 配置Gin模式和日志
|
||||
configureGin()
|
||||
|
||||
// 创建Gin引擎
|
||||
router := gin.New()
|
||||
|
||||
// 添加恢复中间件
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
// 添加日志中间件
|
||||
router.Use(middleware.WrapHandler())
|
||||
|
||||
// 加载模板
|
||||
if err := loadTemplates(router); err != nil {
|
||||
logrus.WithError(err).Fatal("模板加载失败")
|
||||
}
|
||||
|
||||
// 注册路由
|
||||
registerRoutes(mux)
|
||||
|
||||
// 使用日志中间件包装处理器
|
||||
handler := middleware.WrapHandler(mux)
|
||||
registerRoutes(router)
|
||||
|
||||
return &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
Handler: router,
|
||||
}
|
||||
}
|
||||
|
||||
// loadTemplates 加载模板到Gin引擎
|
||||
func loadTemplates(router *gin.Engine) error {
|
||||
tmpl, err := web.ParseTemplates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
router.SetHTMLTemplate(tmpl)
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerRoutes 注册HTTP路由
|
||||
func registerRoutes(mux *http.ServeMux) {
|
||||
func registerRoutes(router *gin.Engine) {
|
||||
// 使用server包中的路由注册函数
|
||||
server.RegisterRoutes(mux)
|
||||
server.RegisterRoutes(router)
|
||||
}
|
||||
|
||||
// startServer 启动服务器并处理优雅关闭
|
||||
@@ -143,3 +168,20 @@ func startServer(server *http.Server) {
|
||||
logger.LogServerStop()
|
||||
}
|
||||
}
|
||||
|
||||
// configureGin 配置Gin的全局设置
|
||||
func configureGin() {
|
||||
// 禁用Gin的颜色输出,提高控制台兼容性
|
||||
gin.DisableConsoleColor()
|
||||
|
||||
// 设置Gin的输出为丢弃,因为我们使用自定义日志中间件
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
|
||||
// 根据配置设置Gin模式
|
||||
if viper.GetString("app.mode") == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
} else {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package constants
|
||||
|
||||
// 验证码类型常量
|
||||
// VerificationCodeType 定义验证码的类型
|
||||
// 应用程序版本信息
|
||||
const (
|
||||
// VerificationCodeTypeText 文本验证码
|
||||
VerificationCodeTypeText = 1
|
||||
// VerificationCodeTypeImage 图片验证码
|
||||
VerificationCodeTypeImage = 2
|
||||
// AppVersion 应用程序版本号
|
||||
AppVersion = "1.2.0"
|
||||
)
|
||||
|
||||
@@ -2,57 +2,52 @@ package admin
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"networkDev/utils/encrypt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 创建基础控制器实例
|
||||
var apiBaseController = controllers.NewBaseController()
|
||||
|
||||
// APIFragmentHandler 接口列表页面片段处理器
|
||||
func APIFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "apis.html", map[string]interface{}{
|
||||
func APIFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "apis.html", gin.H{
|
||||
"Title": "接口管理",
|
||||
})
|
||||
}
|
||||
|
||||
// APIListHandler 接口列表API处理器
|
||||
func APIListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func APIListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
limit, _ := strconv.Atoi(c.Query("limit"))
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
// 获取应用UUID参数(用于按应用筛选接口)
|
||||
appUUID := strings.TrimSpace(r.URL.Query().Get("app_uuid"))
|
||||
appUUID := strings.TrimSpace(c.Query("app_uuid"))
|
||||
|
||||
// 获取接口类型参数(用于按接口类型筛选)
|
||||
apiTypeStr := strings.TrimSpace(r.URL.Query().Get("api_type"))
|
||||
apiTypeStr := strings.TrimSpace(c.Query("api_type"))
|
||||
var apiType int
|
||||
if apiTypeStr != "" {
|
||||
apiType, _ = strconv.Atoi(apiTypeStr)
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
db, ok := apiBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -73,7 +68,7 @@ func APIListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to count APIs")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "获取接口总数失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,7 +77,7 @@ func APIListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Offset(offset).Limit(limit).Order("created_at DESC").Find(&apis).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to fetch APIs")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "获取接口列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -133,9 +128,9 @@ func APIListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 计算分页信息
|
||||
totalPages := (total + int64(limit) - 1) / int64(limit)
|
||||
|
||||
response := map[string]interface{}{
|
||||
response := gin.H{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"data": gin.H{
|
||||
"apis": responseAPIs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
@@ -144,8 +139,7 @@ func APIListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// getAPIStatusName 获取API状态名称
|
||||
@@ -161,12 +155,7 @@ func getAPIStatusName(status int) string {
|
||||
}
|
||||
|
||||
// APIUpdateHandler 更新接口处理器
|
||||
func APIUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func APIUpdateHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
UUID string `json:"uuid"`
|
||||
Status int `json:"status"`
|
||||
@@ -178,39 +167,36 @@ func APIUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ReturnPrivateKey string `json:"return_private_key"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
if !apiBaseController.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if strings.TrimSpace(req.UUID) == "" {
|
||||
http.Error(w, "接口UUID不能为空", http.StatusBadRequest)
|
||||
apiBaseController.HandleValidationError(c, "接口UUID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != 0 && req.Status != 1 {
|
||||
http.Error(w, "无效的状态值", http.StatusBadRequest)
|
||||
apiBaseController.HandleValidationError(c, "无效的状态值")
|
||||
return
|
||||
}
|
||||
|
||||
if !models.IsValidAlgorithm(req.SubmitAlgorithm) || !models.IsValidAlgorithm(req.ReturnAlgorithm) {
|
||||
http.Error(w, "无效的算法类型", http.StatusBadRequest)
|
||||
apiBaseController.HandleValidationError(c, "无效的算法类型")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
db, ok := apiBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 查找并更新API记录
|
||||
var api models.API
|
||||
if err := db.Where("uuid = ?", strings.TrimSpace(req.UUID)).First(&api).Error; err != nil {
|
||||
http.Error(w, "接口不存在", http.StatusNotFound)
|
||||
apiBaseController.HandleValidationError(c, "接口不存在")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -231,32 +217,18 @@ func APIUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := db.Save(&api).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to update API")
|
||||
http.Error(w, "更新接口失败", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "更新接口失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "接口更新成功",
|
||||
"data": api,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
apiBaseController.HandleSuccess(c, "接口更新成功", api)
|
||||
}
|
||||
|
||||
// APIGetAppsHandler 获取应用列表(用于接口页面的应用选择器)
|
||||
func APIGetAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func APIGetAppsHandler(c *gin.Context) {
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
db, ok := apiBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,26 +236,15 @@ func APIGetAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var apps []models.App
|
||||
if err := db.Select("uuid, name").Order("created_at ASC").Find(&apps).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to fetch apps")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "获取应用列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"data": apps,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
apiBaseController.HandleSuccess(c, "获取应用列表成功", apps)
|
||||
}
|
||||
|
||||
// APIGetTypesHandler 获取接口类型列表API处理器
|
||||
func APIGetTypesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func APIGetTypesHandler(c *gin.Context) {
|
||||
// 构建接口类型列表
|
||||
type APITypeItem struct {
|
||||
Value int `json:"value"`
|
||||
@@ -310,35 +271,25 @@ func APIGetTypesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"data": apiTypes,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
apiBaseController.HandleSuccess(c, "获取接口类型列表成功", apiTypes)
|
||||
}
|
||||
|
||||
func APIGenerateKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func APIGenerateKeysHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
Side string `json:"side"` // submit | return
|
||||
Algorithm int `json:"algorithm"` // 与 models.Algorithm* 对应
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
|
||||
if !apiBaseController.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Side != "submit" && req.Side != "return" {
|
||||
http.Error(w, "side参数必须为submit或return", http.StatusBadRequest)
|
||||
apiBaseController.HandleValidationError(c, "side参数必须为submit或return")
|
||||
return
|
||||
}
|
||||
if !models.IsValidAlgorithm(req.Algorithm) {
|
||||
http.Error(w, "无效的算法类型", http.StatusBadRequest)
|
||||
apiBaseController.HandleValidationError(c, "无效的算法类型")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -355,7 +306,7 @@ func APIGenerateKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
key, err := encrypt.GenerateRC4Key(8) // 生成8字节密钥
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to generate RC4 key")
|
||||
http.Error(w, "生成RC4密钥失败", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "生成RC4密钥失败", err)
|
||||
return
|
||||
}
|
||||
result["public_key"] = ""
|
||||
@@ -365,7 +316,7 @@ func APIGenerateKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
publicKey, privateKey, err := encrypt.GenerateRSAKeyPair(2048)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to generate RSA key pair")
|
||||
http.Error(w, "生成RSA密钥失败", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "生成RSA密钥失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -373,14 +324,14 @@ func APIGenerateKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
publicKeyPEM, err := encrypt.PublicKeyToPEM(publicKey)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to convert public key to PEM")
|
||||
http.Error(w, "转换公钥格式失败", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "转换公钥格式失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
privateKeyPEM, err := encrypt.PrivateKeyToPEM(privateKey)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to convert private key to PEM")
|
||||
http.Error(w, "转换私钥格式失败", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "转换私钥格式失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -391,7 +342,7 @@ func APIGenerateKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
publicKeyPEM, privateKeyPEM, err := encrypt.GenerateRSADynamicKeyPair(2048)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to generate RSA dynamic key pair")
|
||||
http.Error(w, "生成RSA动态密钥失败", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "生成RSA动态密钥失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -402,21 +353,15 @@ func APIGenerateKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
encryptKey, _, err := encrypt.GenerateEasyKey()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to generate Easy encryption key")
|
||||
http.Error(w, "生成易加密密钥失败", http.StatusInternalServerError)
|
||||
apiBaseController.HandleInternalError(c, "生成易加密密钥失败", err)
|
||||
return
|
||||
}
|
||||
result["public_key"] = ""
|
||||
result["private_key"] = encrypt.FormatKeyAsString(encryptKey)
|
||||
default:
|
||||
http.Error(w, "不支持的算法类型", http.StatusBadRequest)
|
||||
apiBaseController.HandleValidationError(c, "不支持的算法类型")
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "生成成功",
|
||||
"data": result,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
apiBaseController.HandleSuccess(c, "生成成功", result)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,52 +1,58 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"networkDev/controllers"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 创建BaseController实例
|
||||
var authBaseController = controllers.NewBaseController()
|
||||
|
||||
// LoginPageHandler 管理员登录页渲染处理器
|
||||
// - 如果已登录则重定向到 /admin
|
||||
// - 否则渲染 web/template/admin/login.html 模板
|
||||
// - 自动清理失效的JWT Cookie,避免刷新时的问题
|
||||
func LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func LoginPageHandler(c *gin.Context) {
|
||||
// 使用带清理功能的JWT校验,避免失效Cookie在登录页面造成问题
|
||||
if IsAdminAuthenticatedWithCleanup(w, r) {
|
||||
http.Redirect(w, r, "/admin", http.StatusFound)
|
||||
if IsAdminAuthenticatedWithCleanup(c) {
|
||||
c.Redirect(http.StatusFound, "/admin")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取或生成CSRF令牌
|
||||
var token string
|
||||
if existingToken := utils.GetCSRFTokenFromCookie(r); existingToken != "" {
|
||||
if existingToken := utils.GetCSRFTokenFromCookie(c); existingToken != "" {
|
||||
// 重用现有的Cookie令牌
|
||||
token = existingToken
|
||||
} else {
|
||||
// 生成新的CSRF令牌并设置到Cookie
|
||||
newToken, err := utils.GenerateCSRFToken()
|
||||
if err != nil {
|
||||
http.Error(w, "生成CSRF令牌失败", http.StatusInternalServerError)
|
||||
c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
||||
"Error": "生成CSRF令牌失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
token = newToken
|
||||
utils.SetCSRFToken(w, token)
|
||||
utils.SetCSRFToken(c, token)
|
||||
}
|
||||
|
||||
// 准备模板数据
|
||||
extraData := map[string]interface{}{
|
||||
extraData := gin.H{
|
||||
"Title": "管理员登录",
|
||||
}
|
||||
data := utils.GetDefaultTemplateData()
|
||||
data := authBaseController.GetDefaultTemplateData()
|
||||
data["CSRFToken"] = token
|
||||
|
||||
// 合并额外数据
|
||||
@@ -54,7 +60,7 @@ func LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
utils.RenderTemplate(w, "login.html", data)
|
||||
c.HTML(http.StatusOK, "login.html", data)
|
||||
}
|
||||
|
||||
// LoginHandler 管理员登录接口
|
||||
@@ -62,46 +68,41 @@ func LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// - 验证用户存在与密码正确性
|
||||
// - 仅允许 Role=0 的管理员登录
|
||||
// - 成功后设置简单的会话Cookie(后续可切换为JWT或更完善的Session)
|
||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func LoginHandler(c *gin.Context) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求参数错误", nil)
|
||||
|
||||
if !authBaseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
if body.Username == "" || body.Password == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名和密码不能为空", nil)
|
||||
return
|
||||
}
|
||||
if body.Captcha == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "验证码不能为空", nil)
|
||||
|
||||
if !authBaseController.ValidateRequired(c, map[string]interface{}{
|
||||
"用户名": body.Username,
|
||||
"密码": body.Password,
|
||||
"验证码": body.Captcha,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
if !VerifyCaptcha(r, body.Captcha) {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "验证码错误", nil)
|
||||
if !VerifyCaptcha(c, body.Captcha) {
|
||||
authBaseController.HandleValidationError(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
// 获取数据库连接
|
||||
db, ok := authBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 通过前缀匹配一次性获取所有管理员相关设置
|
||||
var adminSettings []models.Settings
|
||||
if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
||||
if err := db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,25 +118,25 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
adminPasswordSalt, hasSalt := settingsMap["admin_password_salt"]
|
||||
|
||||
if !hasUsername || !hasPassword || !hasSalt {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户名
|
||||
if body.Username != adminUsername {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码为空的情况(首次登录需要初始化)
|
||||
if adminPassword == "" || adminPasswordSalt == "" {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员账号未初始化,请联系系统管理员", nil)
|
||||
authBaseController.HandleInternalError(c, "管理员账号未初始化,请联系系统管理员", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用盐值验证密码
|
||||
if !utils.VerifyPasswordWithSalt(body.Password, adminPasswordSalt, adminPassword) {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
||||
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,30 +150,30 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 生成JWT令牌
|
||||
token, err := generateJWTTokenForAdmin(adminUser)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成令牌失败", nil)
|
||||
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置JWT Cookie(使用安全配置)
|
||||
cookie := utils.CreateSecureCookie("admin_session", token, utils.GetDefaultCookieMaxAge())
|
||||
http.SetCookie(w, cookie)
|
||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "登录成功", map[string]interface{}{
|
||||
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
||||
"redirect": "/admin",
|
||||
})
|
||||
}
|
||||
|
||||
// LogoutHandler 管理员登出
|
||||
// - 清理JWT Cookie会话
|
||||
// - 清理JWT Cookie
|
||||
// - 确保令牌完全失效
|
||||
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func LogoutHandler(c *gin.Context) {
|
||||
// 清理JWT Cookie
|
||||
clearInvalidJWTCookie(w)
|
||||
clearInvalidJWTCookie(c)
|
||||
|
||||
// 可选:将JWT令牌加入黑名单(需要Redis或数据库支持)
|
||||
// 这里可以实现JWT黑名单机制
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "已退出登录", map[string]interface{}{
|
||||
authBaseController.HandleSuccess(c, "已退出登录", gin.H{
|
||||
"redirect": "/admin/login",
|
||||
})
|
||||
}
|
||||
@@ -180,9 +181,9 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// clearInvalidJWTCookie 清理失效的JWT Cookie
|
||||
// - 统一的Cookie清理函数,确保一致性
|
||||
// - 在JWT校验失败时自动调用,提升安全性和用户体验
|
||||
func clearInvalidJWTCookie(w http.ResponseWriter) {
|
||||
func clearInvalidJWTCookie(c *gin.Context) {
|
||||
cookie := utils.CreateExpiredCookie("admin_session")
|
||||
http.SetCookie(w, cookie)
|
||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||
}
|
||||
|
||||
// getJWTSecret 动态获取当前的JWT密钥
|
||||
@@ -246,18 +247,18 @@ func parseJWTToken(tokenString string) (*JWTClaims, error) {
|
||||
}
|
||||
|
||||
// getJWTCookie 获取JWT cookie的通用函数
|
||||
func getJWTCookie(r *http.Request) (*http.Cookie, error) {
|
||||
return r.Cookie("admin_session")
|
||||
func getJWTCookie(c *gin.Context) (string, error) {
|
||||
return c.Cookie("admin_session")
|
||||
}
|
||||
|
||||
// validateAdminPasswordHash 验证管理员密码哈希的通用函数
|
||||
func validateAdminPasswordHash(claims *JWTClaims, r *http.Request) bool {
|
||||
func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
|
||||
// 【安全修复】验证数据库中的当前密码哈希
|
||||
// 这确保了密码修改后,旧的JWT令牌会失效
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
fmt.Printf("[SECURITY WARNING] Database connection failed during auth - Username=%s, IP=%s\n",
|
||||
claims.Username, r.RemoteAddr)
|
||||
claims.Username, c.ClientIP())
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -265,7 +266,7 @@ func validateAdminPasswordHash(claims *JWTClaims, r *http.Request) bool {
|
||||
var adminPassword models.Settings
|
||||
if err := db.Where("name = ?", "admin_password").First(&adminPassword).Error; err != nil {
|
||||
fmt.Printf("[SECURITY WARNING] Admin password not found in database - Username=%s, IP=%s\n",
|
||||
claims.Username, r.RemoteAddr)
|
||||
claims.Username, c.ClientIP())
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -275,7 +276,7 @@ func validateAdminPasswordHash(claims *JWTClaims, r *http.Request) bool {
|
||||
// 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致
|
||||
if claims.PasswordHash != currentPasswordHash {
|
||||
fmt.Printf("[SECURITY WARNING] Password hash mismatch - JWT token invalidated - Username=%s, IP=%s\n",
|
||||
claims.Username, r.RemoteAddr)
|
||||
claims.Username, c.ClientIP())
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -285,14 +286,14 @@ func validateAdminPasswordHash(claims *JWTClaims, r *http.Request) bool {
|
||||
// IsAdminAuthenticated 判断管理员是否已认证(导出)
|
||||
// - 检查admin_session Cookie中的JWT令牌
|
||||
// - 验证令牌签名、过期时间和用户角色
|
||||
func IsAdminAuthenticated(r *http.Request) bool {
|
||||
cookie, err := getJWTCookie(r)
|
||||
if err != nil || cookie.Value == "" {
|
||||
func IsAdminAuthenticated(c *gin.Context) bool {
|
||||
cookie, err := getJWTCookie(c)
|
||||
if err != nil || cookie == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 解析并验证JWT令牌
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
claims, err := parseJWTToken(cookie)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -300,31 +301,31 @@ func IsAdminAuthenticated(r *http.Request) bool {
|
||||
// 注释:由于这是管理员专用认证函数,不需要额外的角色验证
|
||||
|
||||
// 验证密码哈希
|
||||
return validateAdminPasswordHash(claims, r)
|
||||
return validateAdminPasswordHash(claims, c)
|
||||
}
|
||||
|
||||
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
|
||||
// - 当JWT校验失败时,自动清理失效的Cookie
|
||||
// - 适用于API接口等需要清理失效令牌的场景
|
||||
func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) bool {
|
||||
cookie, err := getJWTCookie(r)
|
||||
if err != nil || cookie.Value == "" {
|
||||
func IsAdminAuthenticatedWithCleanup(c *gin.Context) bool {
|
||||
cookie, err := getJWTCookie(c)
|
||||
if err != nil || cookie == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 解析并验证JWT令牌
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
claims, err := parseJWTToken(cookie)
|
||||
if err != nil {
|
||||
// JWT解析失败,清理失效Cookie
|
||||
clearInvalidJWTCookie(w)
|
||||
clearInvalidJWTCookie(c)
|
||||
return false
|
||||
}
|
||||
|
||||
// 注释:由于这是管理员专用认证函数,不需要额外的角色验证
|
||||
|
||||
// 验证密码哈希
|
||||
if !validateAdminPasswordHash(claims, r) {
|
||||
clearInvalidJWTCookie(w)
|
||||
if !validateAdminPasswordHash(claims, c) {
|
||||
clearInvalidJWTCookie(c)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -335,13 +336,13 @@ func IsAdminAuthenticatedWithCleanup(w http.ResponseWriter, r *http.Request) boo
|
||||
// - 从JWT令牌中提取用户信息
|
||||
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
||||
// - 返回用户ID、用户名和角色
|
||||
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
||||
cookie, err := getJWTCookie(r)
|
||||
func GetCurrentAdminUser(c *gin.Context) (*JWTClaims, error) {
|
||||
cookie, err := getJWTCookie(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("未找到会话信息")
|
||||
}
|
||||
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
claims, err := parseJWTToken(cookie)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的会话信息")
|
||||
}
|
||||
@@ -355,40 +356,34 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
||||
// - 从JWT令牌中提取用户信息
|
||||
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
||||
// - 返回用户ID、用户名、角色和是否刷新了令牌
|
||||
func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JWTClaims, bool, error) {
|
||||
cookie, err := getJWTCookie(r)
|
||||
func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
||||
cookie, err := getJWTCookie(c)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("未找到会话信息")
|
||||
}
|
||||
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
claims, err := parseJWTToken(cookie)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("无效的会话信息")
|
||||
}
|
||||
|
||||
// 注释:由于这是管理员专用函数,不需要额外的角色验证
|
||||
|
||||
// 验证密码哈希
|
||||
if !validateAdminPasswordHash(claims, r) {
|
||||
if !validateAdminPasswordHash(claims, c) {
|
||||
return nil, false, fmt.Errorf("会话已失效,请重新登录")
|
||||
}
|
||||
|
||||
// 检查是否需要刷新令牌(根据配置的阈值)
|
||||
// 检查是否需要刷新令牌
|
||||
refreshed := false
|
||||
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh")) * time.Hour
|
||||
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
||||
// 为管理员生成新的JWT令牌
|
||||
adminUser := models.User{
|
||||
Username: claims.Username,
|
||||
}
|
||||
newToken, err := generateJWTTokenForAdmin(adminUser)
|
||||
if err == nil {
|
||||
// 更新Cookie(使用安全配置)
|
||||
newCookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge())
|
||||
http.SetCookie(w, newCookie)
|
||||
c.SetCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
|
||||
refreshed = true
|
||||
|
||||
// 更新claims的过期时间
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(24 * time.Hour))
|
||||
claims.IssuedAt = jwt.NewNumericDate(time.Now())
|
||||
}
|
||||
@@ -400,24 +395,30 @@ func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JW
|
||||
// AdminAuthRequired 管理员认证拦截中间件
|
||||
// - 未登录:重定向到 /admin/login
|
||||
// - 已登录:自动刷新接近过期的令牌,然后放行到后续处理器
|
||||
func AdminAuthRequired(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
func AdminAuthRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 尝试获取用户信息并自动刷新令牌
|
||||
claims, refreshed, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||
claims, refreshed, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
// 自动清理失效的JWT Cookie,提升安全性和用户体验
|
||||
clearInvalidJWTCookie(w)
|
||||
clearInvalidJWTCookie(c)
|
||||
|
||||
// 中文注释:区分普通页面请求与AJAX/JSON请求
|
||||
// - 对 AJAX/JSON:直接返回 401 JSON,便于前端处理(如提示重新登录)
|
||||
// - 对普通页面:保持原有重定向到登录页
|
||||
accept := r.Header.Get("Accept")
|
||||
xrw := strings.ToLower(strings.TrimSpace(r.Header.Get("X-Requested-With")))
|
||||
accept := c.GetHeader("Accept")
|
||||
xrw := strings.ToLower(strings.TrimSpace(c.GetHeader("X-Requested-With")))
|
||||
if strings.Contains(accept, "application/json") || xrw == "xmlhttprequest" {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "未登录或会话已过期",
|
||||
"data": nil,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/login", http.StatusFound)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -427,6 +428,6 @@ func AdminAuthRequired(next http.HandlerFunc) http.HandlerFunc {
|
||||
_ = claims // 避免未使用变量警告
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,21 @@ package admin
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"networkDev/controllers"
|
||||
"networkDev/utils"
|
||||
|
||||
"github.com/mojocn/base64Captcha"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 创建基础控制器实例
|
||||
var captchaBaseController = controllers.NewBaseController()
|
||||
|
||||
// 全局验证码存储器
|
||||
var store = base64Captcha.DefaultMemStore
|
||||
|
||||
@@ -28,17 +32,12 @@ func secureRandomInt(max int) (int, error) {
|
||||
|
||||
// CaptchaHandler 生成验证码图片
|
||||
// GET /admin/captcha - 返回验证码图片
|
||||
func CaptchaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func CaptchaHandler(c *gin.Context) {
|
||||
// 随机生成4-6位长度
|
||||
// 使用crypto/rand生成安全的随机数
|
||||
randomNum, err := secureRandomInt(3)
|
||||
if err != nil {
|
||||
http.Error(w, "生成随机数失败", http.StatusInternalServerError)
|
||||
captchaBaseController.HandleInternalError(c, "生成随机数失败", err)
|
||||
return
|
||||
}
|
||||
captchaLength := 4 + randomNum // 4-6位随机长度
|
||||
@@ -58,20 +57,20 @@ func CaptchaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
captcha := base64Captcha.NewCaptcha(&driver, store)
|
||||
id, b64s, _, err := captcha.Generate()
|
||||
if err != nil {
|
||||
http.Error(w, "生成验证码失败", http.StatusInternalServerError)
|
||||
captchaBaseController.HandleInternalError(c, "生成验证码失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 将验证码ID存储到session中(这里简化处理,实际项目中应该使用更安全的方式)
|
||||
// 设置cookie来存储验证码ID
|
||||
cookie := utils.CreateSecureCookie("captcha_id", id, 300) // 5分钟过期
|
||||
http.SetCookie(w, cookie)
|
||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
||||
|
||||
// 解码base64图片数据并返回
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
c.Header("Content-Type", "image/png")
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
|
||||
// 直接返回base64编码的图片数据,让浏览器解析
|
||||
// 但是我们需要返回实际的图片数据,所以需要解码base64
|
||||
@@ -81,29 +80,30 @@ func CaptchaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
imgData, err := base64.StdEncoding.DecodeString(b64s)
|
||||
if err != nil {
|
||||
http.Error(w, "解码验证码图片失败", http.StatusInternalServerError)
|
||||
captchaBaseController.HandleInternalError(c, "解码验证码图片失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(imgData)
|
||||
c.Data(http.StatusOK, "image/png", imgData)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// VerifyCaptcha 验证验证码
|
||||
// 这个函数将在登录处理中被调用
|
||||
// 支持大小写不敏感匹配
|
||||
func VerifyCaptcha(r *http.Request, captchaValue string) bool {
|
||||
func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
|
||||
// 检查是否为开发模式,如果是则跳过验证码验证
|
||||
if viper.GetBool("server.dev_mode") {
|
||||
return true
|
||||
}
|
||||
|
||||
// 从cookie中获取验证码ID
|
||||
cookie, err := r.Cookie("captcha_id")
|
||||
captchaId, err := c.Cookie("captcha_id")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
captchaId := cookie.Value
|
||||
if captchaId == "" {
|
||||
return false
|
||||
}
|
||||
@@ -132,36 +132,19 @@ func VerifyCaptcha(r *http.Request, captchaValue string) bool {
|
||||
|
||||
// CaptchaAPIHandler 验证码API接口(可选,用于AJAX验证)
|
||||
// POST /admin/api/captcha/verify - 验证验证码
|
||||
func CaptchaAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func CaptchaAPIHandler(c *gin.Context) {
|
||||
var body struct {
|
||||
Captcha string `json:"captcha"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "请求参数错误",
|
||||
})
|
||||
if !captchaBaseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
isValid := VerifyCaptcha(r, body.Captcha)
|
||||
isValid := VerifyCaptcha(c, body.Captcha)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if isValid {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "验证码正确",
|
||||
})
|
||||
captchaBaseController.HandleSuccess(c, "验证码正确", nil)
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 1,
|
||||
"msg": "验证码错误",
|
||||
})
|
||||
captchaBaseController.HandleValidationError(c, "验证码错误")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,20 @@ package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/constants"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"networkDev/services"
|
||||
"networkDev/utils"
|
||||
"networkDev/utils/timeutil"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 创建基础控制器实例
|
||||
var handlersBaseController = controllers.NewBaseController()
|
||||
|
||||
// formatDBType 格式化数据库类型显示
|
||||
// 将配置文件中的小写类型转换为友好的显示格式
|
||||
func formatDBType(dbType string) string {
|
||||
@@ -32,40 +37,40 @@ func formatDBType(dbType string) string {
|
||||
// - 未登录:重定向到 /admin/login
|
||||
// - 已登录:渲染后台布局页(或重定向到 /admin/layout)
|
||||
// - 自动清理失效的JWT Cookie
|
||||
func AdminIndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if IsAdminAuthenticatedWithCleanup(w, r) {
|
||||
func AdminIndexHandler(c *gin.Context) {
|
||||
if IsAdminAuthenticatedWithCleanup(c) {
|
||||
// 直接渲染布局页,保持URL为 /admin
|
||||
AdminLayoutHandler(w, r)
|
||||
AdminLayoutHandler(c)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/login", http.StatusFound)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
}
|
||||
|
||||
// AdminLayoutHandler 后台布局页渲染
|
||||
// - 渲染 layout.html,包含顶部导航、侧边栏与动态内容容器
|
||||
func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func AdminLayoutHandler(c *gin.Context) {
|
||||
// 获取或生成CSRF令牌
|
||||
var token string
|
||||
if existingToken := utils.GetCSRFTokenFromCookie(r); existingToken != "" {
|
||||
if existingToken := utils.GetCSRFTokenFromCookie(c); existingToken != "" {
|
||||
// 重用现有的Cookie令牌
|
||||
token = existingToken
|
||||
} else {
|
||||
// 生成新的CSRF令牌并设置到Cookie
|
||||
newToken, err := utils.GenerateCSRFToken()
|
||||
if err != nil {
|
||||
http.Error(w, "生成CSRF令牌失败", http.StatusInternalServerError)
|
||||
handlersBaseController.HandleInternalError(c, "生成CSRF令牌失败", err)
|
||||
return
|
||||
}
|
||||
token = newToken
|
||||
utils.SetCSRFToken(w, token)
|
||||
utils.SetCSRFToken(c, token)
|
||||
}
|
||||
|
||||
// 准备额外的模板数据
|
||||
extraData := make(map[string]interface{})
|
||||
extraData := gin.H{}
|
||||
|
||||
// 从数据库读取站点标题
|
||||
db, dbErr := database.GetDB()
|
||||
if dbErr != nil {
|
||||
db, ok := handlersBaseController.GetDB(c)
|
||||
if !ok {
|
||||
extraData["Title"] = "凌动技术"
|
||||
} else {
|
||||
siteTitle, settingErr := services.FindSettingByName("site_title", db)
|
||||
@@ -77,7 +82,7 @@ func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 准备模板数据
|
||||
data := utils.GetDefaultTemplateData()
|
||||
data := handlersBaseController.GetDefaultTemplateData()
|
||||
data["CSRFToken"] = token
|
||||
|
||||
// 合并额外数据
|
||||
@@ -85,13 +90,13 @@ func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
utils.RenderTemplate(w, "layout.html", data)
|
||||
c.HTML(http.StatusOK, "layout.html", data)
|
||||
}
|
||||
|
||||
// DashboardFragmentHandler 仪表盘片段渲染
|
||||
// - 展示系统信息:版本、开发模式、数据库类型、启动时长
|
||||
func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
version := "1.0.0"
|
||||
func DashboardFragmentHandler(c *gin.Context) {
|
||||
version := constants.AppVersion
|
||||
mode := viper.GetBool("server.dev_mode")
|
||||
dbType := viper.GetString("database.type")
|
||||
if dbType == "" {
|
||||
@@ -99,25 +104,20 @@ func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
uptime := timeutil.GetServerUptimeString()
|
||||
|
||||
data := map[string]interface{}{
|
||||
data := gin.H{
|
||||
"Version": version,
|
||||
"Mode": mode,
|
||||
"DBType": formatDBType(dbType),
|
||||
"Uptime": uptime,
|
||||
}
|
||||
|
||||
utils.RenderTemplate(w, "dashboard.html", data)
|
||||
c.HTML(http.StatusOK, "dashboard.html", data)
|
||||
}
|
||||
|
||||
// SystemInfoHandler 系统信息API接口
|
||||
// - 返回系统运行状态的JSON数据,用于前端定时刷新
|
||||
func SystemInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
version := "1.0.0"
|
||||
func SystemInfoHandler(c *gin.Context) {
|
||||
version := constants.AppVersion
|
||||
mode := viper.GetBool("server.dev_mode")
|
||||
dbType := viper.GetString("database.type")
|
||||
if dbType == "" {
|
||||
@@ -125,28 +125,22 @@ func SystemInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
uptime := timeutil.GetServerUptimeString()
|
||||
|
||||
data := map[string]interface{}{
|
||||
data := gin.H{
|
||||
"version": version,
|
||||
"mode": mode,
|
||||
"db_type": formatDBType(dbType),
|
||||
"uptime": uptime,
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", data)
|
||||
handlersBaseController.HandleSuccess(c, "ok", data)
|
||||
}
|
||||
|
||||
// DashboardStatsHandler 仪表盘统计数据API接口
|
||||
// - 返回应用统计数据的JSON数据,包括全部/启用/禁用/变量数量
|
||||
func DashboardStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func DashboardStatsHandler(c *gin.Context) {
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
db, ok := handlersBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -158,34 +152,34 @@ func DashboardStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 统计全部应用数量
|
||||
if err := db.Model(&models.App{}).Count(&totalApps).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计应用数量失败", nil)
|
||||
handlersBaseController.HandleInternalError(c, "统计应用数量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 统计启用应用数量
|
||||
if err := db.Model(&models.App{}).Where("status = ?", 1).Count(&enabledApps).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计启用应用数量失败", nil)
|
||||
handlersBaseController.HandleInternalError(c, "统计启用应用数量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 统计禁用应用数量
|
||||
if err := db.Model(&models.App{}).Where("status = ?", 0).Count(&disabledApps).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计禁用应用数量失败", nil)
|
||||
handlersBaseController.HandleInternalError(c, "统计禁用应用数量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 统计变量数量
|
||||
if err := db.Model(&models.Variable{}).Count(&totalVariables).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计变量数量失败", nil)
|
||||
handlersBaseController.HandleInternalError(c, "统计变量数量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
data := gin.H{
|
||||
"total_apps": totalApps,
|
||||
"enabled_apps": enabledApps,
|
||||
"disabled_apps": disabledApps,
|
||||
"total_variables": totalVariables,
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", data)
|
||||
handlersBaseController.HandleSuccess(c, "ok", data)
|
||||
}
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
|
||||
// 新增:用于刷新内存缓存
|
||||
"networkDev/services"
|
||||
// 新增:用于RedisDel上下文
|
||||
"context"
|
||||
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"networkDev/services"
|
||||
"networkDev/utils"
|
||||
)
|
||||
|
||||
// 创建基础控制器实例
|
||||
var settingsBaseController = controllers.NewBaseController()
|
||||
|
||||
// SettingsFragmentHandler 设置片段渲染
|
||||
// - 渲染设置表单(通过前端JS调用API加载/保存)
|
||||
func SettingsFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "settings.html", map[string]interface{}{})
|
||||
func SettingsFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "settings.html", gin.H{})
|
||||
}
|
||||
|
||||
// SettingsQueryHandler 设置查询API
|
||||
// - 返回所有设置项的 name:value 映射
|
||||
func SettingsQueryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
func SettingsQueryHandler(c *gin.Context) {
|
||||
db, ok := settingsBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var list []models.Settings
|
||||
if err := db.Find(&list).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
settingsBaseController.HandleInternalError(c, "查询失败", err)
|
||||
return
|
||||
}
|
||||
res := map[string]string{}
|
||||
for _, s := range list {
|
||||
res[s.Name] = s.Value
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", res)
|
||||
settingsBaseController.HandleSuccess(c, "ok", res)
|
||||
}
|
||||
|
||||
// SettingsUpdateHandler 更新系统设置处理器
|
||||
@@ -56,16 +50,10 @@ func SettingsQueryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// - 更新完成后:
|
||||
// 1. 删除对应的Redis缓存键,确保后续读取走数据库并重建缓存
|
||||
// 2. 刷新SettingsService内存缓存
|
||||
func SettingsUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func SettingsUpdateHandler(c *gin.Context) {
|
||||
// 先尝试解析为直接字段格式
|
||||
var directBody map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&directBody); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
if !settingsBaseController.BindJSON(c, &directBody) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,7 +70,7 @@ func SettingsUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "settings字段格式错误", nil)
|
||||
settingsBaseController.HandleValidationError(c, "settings字段格式错误")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -99,13 +87,12 @@ func SettingsUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if len(settingsData) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "无设置项", nil)
|
||||
settingsBaseController.HandleValidationError(c, "无设置项")
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
db, ok := settingsBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +107,7 @@ func SettingsUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s = models.Settings{Name: k, Value: v}
|
||||
if err := db.Create(&s).Error; err != nil {
|
||||
logrus.WithError(err).WithField("setting_name", k).Error("创建设置失败")
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, fmt.Sprintf("保存设置 %s 失败", k), nil)
|
||||
settingsBaseController.HandleInternalError(c, fmt.Sprintf("保存设置 %s 失败", k), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -128,7 +115,7 @@ func SettingsUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 存在则更新
|
||||
if err := db.Model(&models.Settings{}).Where("id = ?", s.ID).Update("value", v).Error; err != nil {
|
||||
logrus.WithError(err).WithField("setting_name", k).Error("更新设置失败")
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, fmt.Sprintf("更新设置 %s 失败", k), nil)
|
||||
settingsBaseController.HandleInternalError(c, fmt.Sprintf("更新设置 %s 失败", k), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -143,5 +130,5 @@ func SettingsUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 刷新内存中的设置缓存,保证后续读取一致
|
||||
services.GetSettingsService().RefreshCache()
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "保存成功", nil)
|
||||
settingsBaseController.HandleSuccess(c, "保存成功", nil)
|
||||
}
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 创建基础控制器实例
|
||||
var baseController = controllers.NewBaseController()
|
||||
|
||||
// UserFragmentHandler 个人资料片段渲染
|
||||
// - 渲染个人资料与修改密码表单
|
||||
func UserFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "user.html", map[string]interface{}{})
|
||||
func UserFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "user.html", gin.H{})
|
||||
}
|
||||
|
||||
// UserProfileQueryHandler 获取当前登录管理员的用户名
|
||||
// - 返回 JSON: {username}
|
||||
// - 直接从JWT获取用户名信息
|
||||
func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||
func UserProfileQueryHandler(c *gin.Context) {
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||
baseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", map[string]interface{}{
|
||||
baseController.HandleSuccess(c, "ok", gin.H{
|
||||
"username": claims.Username,
|
||||
})
|
||||
}
|
||||
@@ -40,15 +39,10 @@ func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// - 校验旧密码正确性、新密码与确认一致性
|
||||
// - 成功后更新密码哈希
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||
func UserPasswordUpdateHandler(c *gin.Context) {
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||
baseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,43 +51,45 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
NewPassword string `json:"new_password"`
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
}
|
||||
var decodeErr error
|
||||
if decodeErr = json.NewDecoder(r.Body).Decode(&body); decodeErr != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求参数错误", nil)
|
||||
|
||||
if !baseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
// 基础校验
|
||||
if body.OldPassword == "" || body.NewPassword == "" || body.ConfirmPassword == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "旧密码/新密码/确认密码均不能为空", nil)
|
||||
if !baseController.ValidateRequired(c, map[string]interface{}{
|
||||
"旧密码": body.OldPassword,
|
||||
"新密码": body.NewPassword,
|
||||
"确认密码": body.ConfirmPassword,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(body.NewPassword) < 6 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "新密码长度不能少于6位", nil)
|
||||
baseController.HandleValidationError(c, "新密码长度不能少于6位")
|
||||
return
|
||||
}
|
||||
if body.NewPassword != body.ConfirmPassword {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "两次输入的新密码不一致", nil)
|
||||
baseController.HandleValidationError(c, "两次输入的新密码不一致")
|
||||
return
|
||||
}
|
||||
if body.NewPassword == body.OldPassword {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "新密码不能与旧密码相同", nil)
|
||||
baseController.HandleValidationError(c, "新密码不能与旧密码相同")
|
||||
return
|
||||
}
|
||||
|
||||
// 注释:由于使用了AdminAuthRequired中间件,已确保是管理员用户
|
||||
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
db, ok := baseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 通过前缀匹配一次性获取所有管理员相关设置
|
||||
var adminSettings []models.Settings
|
||||
if err = db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "获取管理员设置失败", nil)
|
||||
baseController.HandleInternalError(c, "获取管理员设置失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -107,37 +103,37 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
adminPassword, hasPassword := settingsMap["admin_password"]
|
||||
adminPasswordSalt, hasSalt := settingsMap["admin_password_salt"]
|
||||
if !hasPassword || !hasSalt {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码设置不完整", nil)
|
||||
baseController.HandleInternalError(c, "管理员密码设置不完整", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 校验旧密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "旧密码不正确", nil)
|
||||
baseController.HandleValidationError(c, "旧密码不正确")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新的密码盐值
|
||||
newSalt, err := utils.GenerateRandomSalt()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码盐失败", nil)
|
||||
baseController.HandleInternalError(c, "生成密码盐失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新密码哈希
|
||||
newPasswordHash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码哈希失败", nil)
|
||||
baseController.HandleInternalError(c, "生成密码哈希失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新settings中的管理员密码和盐值
|
||||
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password").Update("value", newPasswordHash).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码失败", nil)
|
||||
baseController.HandleInternalError(c, "更新密码失败", err)
|
||||
return
|
||||
}
|
||||
if err = db.Model(&models.Settings{}).Where("name = ?", "admin_password_salt").Update("value", newSalt).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码盐值失败", nil)
|
||||
baseController.HandleInternalError(c, "更新密码盐值失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,16 +145,15 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
newToken, err := generateJWTTokenForAdmin(adminUser)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
||||
baseController.HandleInternalError(c, "生成新令牌失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新Cookie(使用安全配置)
|
||||
cookie := utils.CreateSecureCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge())
|
||||
http.SetCookie(w, cookie)
|
||||
c.SetCookie("admin_session", newToken, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
|
||||
|
||||
// 密码修改成功,已重新生成JWT令牌
|
||||
utils.JsonResponse(w, http.StatusOK, true, "密码修改成功", nil)
|
||||
baseController.HandleSuccess(c, "密码修改成功", nil)
|
||||
}
|
||||
|
||||
// UserProfileUpdateHandler 修改当前登录管理员的用户名
|
||||
@@ -166,15 +161,10 @@ func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// - 校验用户名非空、长度与唯一性
|
||||
// - 更新数据库后重新签发JWT并写入 Cookie,保持前端展示的一致性
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
_, _, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||
func UserProfileUpdateHandler(c *gin.Context) {
|
||||
_, _, err := GetCurrentAdminUserWithRefresh(c)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||
baseController.HandleValidationError(c, "未登录或会话已过期")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,24 +172,22 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Username string `json:"username"`
|
||||
OldPassword string `json:"old_password"`
|
||||
}
|
||||
if decodeErr := json.NewDecoder(r.Body).Decode(&body); decodeErr != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求参数错误", nil)
|
||||
if !baseController.BindJSON(c, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(body.Username)
|
||||
if username == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名不能为空", nil)
|
||||
baseController.HandleValidationError(c, "用户名不能为空")
|
||||
return
|
||||
}
|
||||
if len(username) > 64 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名长度不能超过64字符", nil)
|
||||
baseController.HandleValidationError(c, "用户名长度不能超过64字符")
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
db, ok := baseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,7 +196,7 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取所有管理员相关设置
|
||||
var adminSettings []models.Settings
|
||||
if dbErr := db.Where("name LIKE ?", "admin_%").Find(&adminSettings).Error; dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "获取管理员设置失败", nil)
|
||||
baseController.HandleInternalError(c, "获取管理员设置失败", dbErr)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -220,25 +208,25 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
adminUsername, exists := settingsMap["admin_username"]
|
||||
if !exists {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员用户名设置不存在", nil)
|
||||
baseController.HandleInternalError(c, "管理员用户名设置不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
adminPassword, exists := settingsMap["admin_password"]
|
||||
if !exists {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码设置不存在", nil)
|
||||
baseController.HandleInternalError(c, "管理员密码设置不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
adminPasswordSalt, exists := settingsMap["admin_password_salt"]
|
||||
if !exists {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "管理员密码盐值设置不存在", nil)
|
||||
baseController.HandleInternalError(c, "管理员密码盐值设置不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果用户名未变化则直接返回成功(无需校验旧密码)
|
||||
if strings.EqualFold(username, adminUsername) {
|
||||
utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{
|
||||
baseController.HandleSuccess(c, "保存成功", gin.H{
|
||||
"username": username,
|
||||
})
|
||||
return
|
||||
@@ -246,19 +234,19 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 修改用户名需要进行当前密码校验
|
||||
if strings.TrimSpace(body.OldPassword) == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "修改用户名需要提供当前密码", nil)
|
||||
baseController.HandleValidationError(c, "修改用户名需要提供当前密码")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用盐值验证当前密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, adminPasswordSalt, adminPassword) {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "当前密码不正确", nil)
|
||||
baseController.HandleValidationError(c, "当前密码不正确")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新管理员用户名设置
|
||||
if dbErr := db.Model(&models.Settings{}).Where("name = ?", "admin_username").Update("value", username).Error; dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新管理员用户名失败", nil)
|
||||
baseController.HandleInternalError(c, "更新管理员用户名失败", dbErr)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -271,13 +259,12 @@ func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
token, err := generateJWTTokenForAdmin(adminUser)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
||||
baseController.HandleInternalError(c, "生成新令牌失败", err)
|
||||
return
|
||||
}
|
||||
cookie := utils.CreateSecureCookie("admin_session", token, utils.GetDefaultCookieMaxAge())
|
||||
http.SetCookie(w, cookie)
|
||||
c.SetCookie("admin_session", token, utils.GetDefaultCookieMaxAge(), "/", "", false, true)
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{
|
||||
baseController.HandleSuccess(c, "保存成功", gin.H{
|
||||
"username": username,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,58 +1,57 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/controllers"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 创建基础控制器实例
|
||||
var variableBaseController = controllers.NewBaseController()
|
||||
|
||||
// VariableFragmentHandler 变量列表页面片段处理器
|
||||
func VariableFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "variables", map[string]interface{}{
|
||||
func VariableFragmentHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "variables.html", gin.H{
|
||||
"Title": "变量管理",
|
||||
})
|
||||
}
|
||||
|
||||
// VariableListHandler 变量列表API处理器
|
||||
func VariableListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func VariableListHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
limit, _ := strconv.Atoi(c.Query("limit"))
|
||||
// 兼容前端使用的page_size参数
|
||||
if limit <= 0 {
|
||||
limit, _ = strconv.Atoi(c.Query("page_size"))
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
// 获取应用UUID参数(用于按应用筛选变量)
|
||||
appUUID := strings.TrimSpace(r.URL.Query().Get("app_uuid"))
|
||||
appUUID := strings.TrimSpace(c.Query("app_uuid"))
|
||||
|
||||
// 获取搜索关键词参数(支持编号、别名、数据的综合搜索)
|
||||
search := strings.TrimSpace(r.URL.Query().Get("search"))
|
||||
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
|
||||
// 兼容旧的别名搜索参数
|
||||
if search == "" {
|
||||
search = strings.TrimSpace(r.URL.Query().Get("alias"))
|
||||
search = strings.TrimSpace(c.Query("alias"))
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
db, ok := variableBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ func VariableListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 如果指定了搜索关键词,则在编号、别名、数据、备注中进行模糊搜索
|
||||
if search != "" {
|
||||
query = query.Where("number LIKE ? OR alias LIKE ? OR data LIKE ? OR remark LIKE ?",
|
||||
query = query.Where("number LIKE ? OR alias LIKE ? OR data LIKE ? OR remark LIKE ?",
|
||||
"%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
@@ -74,7 +73,7 @@ func VariableListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to count variables")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
variableBaseController.HandleInternalError(c, "查询变量总数失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,7 +82,7 @@ func VariableListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Offset(offset).Limit(limit).Order("created_at DESC").Find(&variables).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to fetch variables")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
variableBaseController.HandleInternalError(c, "查询变量列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -141,24 +140,18 @@ func VariableListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
response := gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"count": total,
|
||||
"data": responseData,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
utils.WriteJSONResponse(w, http.StatusOK, response)
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// VariableCreateHandler 新增变量API处理器
|
||||
func VariableCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func VariableCreateHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
AppUUID string `json:"app_uuid"`
|
||||
Alias string `json:"alias"`
|
||||
@@ -166,40 +159,34 @@ func VariableCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
logrus.WithError(err).Error("Failed to decode JSON request")
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
if !variableBaseController.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if strings.TrimSpace(req.AppUUID) == "" {
|
||||
http.Error(w, "应用UUID不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Alias) == "" {
|
||||
http.Error(w, "变量别名不能为空", http.StatusBadRequest)
|
||||
if !variableBaseController.ValidateRequired(c, map[string]interface{}{
|
||||
"应用UUID": req.AppUUID,
|
||||
"变量别名": req.Alias,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证别名格式:必须以英文字母开头,只能包含数字和英文字母
|
||||
aliasPattern := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]*$`)
|
||||
if !aliasPattern.MatchString(req.Alias) {
|
||||
http.Error(w, "别名必须以英文字母开头,只能包含数字和英文字母", http.StatusBadRequest)
|
||||
variableBaseController.HandleValidationError(c, "别名必须以英文字母开头,只能包含数字和英文字母")
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
db, ok := variableBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证应用是否存在
|
||||
var app models.App
|
||||
if err := db.Where("uuid = ?", req.AppUUID).First(&app).Error; err != nil {
|
||||
http.Error(w, "应用不存在", http.StatusBadRequest)
|
||||
variableBaseController.HandleValidationError(c, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -213,27 +200,15 @@ func VariableCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := db.Create(&variable).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to create variable")
|
||||
http.Error(w, "创建变量失败", http.StatusInternalServerError)
|
||||
variableBaseController.HandleInternalError(c, "创建变量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "创建成功",
|
||||
"data": variable,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
variableBaseController.HandleSuccess(c, "创建成功", variable)
|
||||
}
|
||||
|
||||
// VariableUpdateHandler 更新变量API处理器
|
||||
func VariableUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func VariableUpdateHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
UUID string `json:"uuid"`
|
||||
AppUUID string `json:"app_uuid"`
|
||||
@@ -242,50 +217,42 @@ func VariableUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
if !variableBaseController.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if strings.TrimSpace(req.UUID) == "" {
|
||||
http.Error(w, "变量UUID不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.AppUUID) == "" {
|
||||
http.Error(w, "应用UUID不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Alias) == "" {
|
||||
http.Error(w, "变量别名不能为空", http.StatusBadRequest)
|
||||
if !variableBaseController.ValidateRequired(c, map[string]interface{}{
|
||||
"变量UUID": req.UUID,
|
||||
"应用UUID": req.AppUUID,
|
||||
"变量别名": req.Alias,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证别名格式:必须以英文字母开头,只能包含数字和英文字母
|
||||
aliasPattern := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]*$`)
|
||||
if !aliasPattern.MatchString(req.Alias) {
|
||||
http.Error(w, "别名必须以英文字母开头,只能包含数字和英文字母", http.StatusBadRequest)
|
||||
variableBaseController.HandleValidationError(c, "别名必须以英文字母开头,只能包含数字和英文字母")
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
db, ok := variableBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证应用是否存在
|
||||
var app models.App
|
||||
if err := db.Where("uuid = ?", req.AppUUID).First(&app).Error; err != nil {
|
||||
http.Error(w, "应用不存在", http.StatusBadRequest)
|
||||
variableBaseController.HandleValidationError(c, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 通过uuid字段查找变量
|
||||
var variable models.Variable
|
||||
if err := db.Where("uuid = ?", strings.TrimSpace(req.UUID)).First(&variable).Error; err != nil {
|
||||
http.Error(w, "变量不存在", http.StatusNotFound)
|
||||
variableBaseController.HandleValidationError(c, "变量不存在")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -297,139 +264,90 @@ func VariableUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := db.Save(&variable).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to update variable")
|
||||
http.Error(w, "更新变量失败", http.StatusInternalServerError)
|
||||
variableBaseController.HandleInternalError(c, "更新变量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "更新成功",
|
||||
"data": variable,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
variableBaseController.HandleSuccess(c, "更新成功", variable)
|
||||
}
|
||||
|
||||
// VariableDeleteHandler 删除变量API处理器
|
||||
func VariableDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func VariableDeleteHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
if !variableBaseController.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.ID == 0 {
|
||||
http.Error(w, "变量ID不能为空", http.StatusBadRequest)
|
||||
variableBaseController.HandleValidationError(c, "变量ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
db, ok := variableBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 删除变量
|
||||
if err := db.Delete(&models.Variable{}, req.ID).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to delete variable")
|
||||
http.Error(w, "删除变量失败", http.StatusInternalServerError)
|
||||
variableBaseController.HandleInternalError(c, "删除变量失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("variable_id", req.ID).Info("Successfully deleted variable")
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "删除成功",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
variableBaseController.HandleSuccess(c, "删除成功", nil)
|
||||
}
|
||||
|
||||
// VariablesBatchDeleteHandler 批量删除变量API处理器
|
||||
func VariablesBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
func VariablesBatchDeleteHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
if !variableBaseController.BindJSON(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
http.Error(w, "请选择要删除的变量", http.StatusBadRequest)
|
||||
variableBaseController.HandleValidationError(c, "请选择要删除的变量")
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
db, ok := variableBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 批量删除变量
|
||||
if err := db.Delete(&models.Variable{}, req.IDs).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to batch delete variables")
|
||||
http.Error(w, "批量删除失败", http.StatusInternalServerError)
|
||||
variableBaseController.HandleInternalError(c, "批量删除失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("variable_ids", req.IDs).Info("Successfully batch deleted variables")
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "批量删除成功",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
variableBaseController.HandleSuccess(c, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
// VariableGetAppsHandler 获取应用列表(用于筛选下拉框)
|
||||
func VariableGetAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
func VariableGetAppsHandler(c *gin.Context) {
|
||||
db, ok := variableBaseController.GetDB(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var apps []models.App
|
||||
if err := db.Select("uuid, name").Find(&apps).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to fetch apps")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
variableBaseController.HandleInternalError(c, "获取应用列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": apps,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
utils.WriteJSONResponse(w, http.StatusOK, response)
|
||||
variableBaseController.HandleSuccess(c, "success", apps)
|
||||
}
|
||||
159
controllers/base.go
Normal file
159
controllers/base.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"networkDev/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BaseController 基础控制器结构体
|
||||
type BaseController struct{}
|
||||
|
||||
// NewBaseController 创建基础控制器实例
|
||||
func NewBaseController() *BaseController {
|
||||
return &BaseController{}
|
||||
}
|
||||
|
||||
// GetDB 获取数据库连接,统一错误处理
|
||||
func (bc *BaseController) GetDB(c *gin.Context) (*gorm.DB, bool) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
bc.HandleDatabaseError(c, err)
|
||||
return nil, false
|
||||
}
|
||||
return db, true
|
||||
}
|
||||
|
||||
// HandleDatabaseError 统一处理数据库连接错误
|
||||
func (bc *BaseController) HandleDatabaseError(c *gin.Context, err error) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "数据库连接失败",
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleValidationError 统一处理验证错误
|
||||
func (bc *BaseController) HandleValidationError(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1,
|
||||
"msg": message,
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleNotFoundError 统一处理资源未找到错误
|
||||
func (bc *BaseController) HandleNotFoundError(c *gin.Context, resource string) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 1,
|
||||
"msg": resource + "不存在",
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleInternalError 统一处理内部服务器错误
|
||||
func (bc *BaseController) HandleInternalError(c *gin.Context, message string, err error) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": message,
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleSuccess 统一处理成功响应
|
||||
func (bc *BaseController) HandleSuccess(c *gin.Context, message string, data interface{}) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": message,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleCreated 统一处理创建成功响应
|
||||
func (bc *BaseController) HandleCreated(c *gin.Context, message string, data interface{}) {
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"code": 0,
|
||||
"msg": message,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateRequired 验证必填字段
|
||||
func (bc *BaseController) ValidateRequired(c *gin.Context, fields map[string]interface{}) bool {
|
||||
for fieldName, fieldValue := range fields {
|
||||
if fieldValue == nil || fieldValue == "" {
|
||||
bc.HandleValidationError(c, fieldName+"不能为空")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPaginationParams 获取分页参数
|
||||
func (bc *BaseController) GetPaginationParams(c *gin.Context) (int, int) {
|
||||
page := 1
|
||||
pageSize := 10
|
||||
|
||||
if p := c.Query("page"); p != "" {
|
||||
if pageInt, err := strconv.Atoi(p); err == nil && pageInt > 0 {
|
||||
page = pageInt
|
||||
}
|
||||
}
|
||||
|
||||
if ps := c.Query("page_size"); ps != "" {
|
||||
if pageSizeInt, err := strconv.Atoi(ps); err == nil && pageSizeInt > 0 && pageSizeInt <= 100 {
|
||||
pageSize = pageSizeInt
|
||||
}
|
||||
}
|
||||
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
// CalculateOffset 计算数据库查询偏移量
|
||||
func (bc *BaseController) CalculateOffset(page, pageSize int) int {
|
||||
return (page - 1) * pageSize
|
||||
}
|
||||
|
||||
// BindJSON 绑定JSON数据并处理错误
|
||||
func (bc *BaseController) BindJSON(c *gin.Context, obj interface{}) bool {
|
||||
if err := c.ShouldBindJSON(obj); err != nil {
|
||||
bc.HandleValidationError(c, "请求参数错误: "+err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// BindQuery 绑定查询参数并处理错误
|
||||
func (bc *BaseController) BindQuery(c *gin.Context, obj interface{}) bool {
|
||||
if err := c.ShouldBindQuery(obj); err != nil {
|
||||
bc.HandleValidationError(c, "查询参数错误: "+err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// BindURI 绑定URI参数并处理错误
|
||||
func (bc *BaseController) BindURI(c *gin.Context, obj interface{}) bool {
|
||||
if err := c.ShouldBindUri(obj); err != nil {
|
||||
bc.HandleValidationError(c, "URI参数绑定失败: "+err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetDefaultTemplateData 获取默认模板数据
|
||||
// 返回包含系统基础信息的数据映射,包括站点标题、页脚文本、备案信息等
|
||||
func (bc *BaseController) GetDefaultTemplateData() gin.H {
|
||||
return gin.H{
|
||||
"SystemName": "凌动技术",
|
||||
"FooterText": "© 2025 凌动技术 保留所有权利",
|
||||
"ICPRecord": "",
|
||||
"ICPRecordLink": "https://beian.miit.gov.cn",
|
||||
"PSBRecord": "",
|
||||
"PSBRecordLink": "https://www.beian.gov.cn",
|
||||
}
|
||||
}
|
||||
@@ -2,72 +2,48 @@ package home
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/controllers"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/services"
|
||||
"networkDev/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RootHandler 主页处理器
|
||||
func RootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
var homeBaseController = controllers.NewBaseController()
|
||||
|
||||
// getSettingValue 获取配置值,优先从数据库获取,不存在时使用默认值
|
||||
func getSettingValue(settingName string, defaultValue string, db *gorm.DB) string {
|
||||
if setting, err := services.FindSettingByName(settingName, db); err == nil {
|
||||
return setting.Value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// RootHandler 主页处理器
|
||||
func RootHandler(c *gin.Context) {
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
||||
"error": "数据库连接失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 从数据库获取站点标题和页脚文本
|
||||
siteTitle, err := services.FindSettingByName("site_title", db)
|
||||
if err != nil {
|
||||
siteTitle = &models.Settings{Value: "凌动技术"}
|
||||
}
|
||||
// 获取默认模板数据
|
||||
defaultData := homeBaseController.GetDefaultTemplateData()
|
||||
|
||||
footerText, err := services.FindSettingByName("footer_text", db)
|
||||
if err != nil {
|
||||
footerText = &models.Settings{Value: "© 2025 凌动技术 保留所有权利"}
|
||||
}
|
||||
|
||||
// 从数据库获取备案信息
|
||||
icpRecord, err := services.FindSettingByName("icp_record", db)
|
||||
if err != nil {
|
||||
icpRecord = &models.Settings{Value: ""}
|
||||
}
|
||||
|
||||
icpRecordLink, err := services.FindSettingByName("icp_record_link", db)
|
||||
if err != nil {
|
||||
icpRecordLink = &models.Settings{Value: "https://beian.miit.gov.cn"}
|
||||
}
|
||||
|
||||
// 从数据库获取公安备案信息
|
||||
psbRecord, err := services.FindSettingByName("psb_record", db)
|
||||
if err != nil {
|
||||
psbRecord = &models.Settings{Value: ""}
|
||||
}
|
||||
|
||||
psbRecordLink, err := services.FindSettingByName("psb_record_link", db)
|
||||
if err != nil {
|
||||
psbRecordLink = &models.Settings{Value: "https://www.beian.gov.cn"}
|
||||
}
|
||||
|
||||
// 准备模板数据
|
||||
data := map[string]interface{}{
|
||||
"SystemName": siteTitle.Value,
|
||||
"FooterText": footerText.Value,
|
||||
"ICPRecord": icpRecord.Value,
|
||||
"ICPRecordLink": icpRecordLink.Value,
|
||||
"PSBRecord": psbRecord.Value,
|
||||
"PSBRecordLink": psbRecordLink.Value,
|
||||
// 准备模板数据,优先使用数据库配置,不存在时使用默认值
|
||||
data := gin.H{
|
||||
"SystemName": getSettingValue("site_title", defaultData["SystemName"].(string), db),
|
||||
"FooterText": getSettingValue("footer_text", defaultData["FooterText"].(string), db),
|
||||
"ICPRecord": getSettingValue("icp_record", defaultData["ICPRecord"].(string), db),
|
||||
"ICPRecordLink": getSettingValue("icp_record_link", defaultData["ICPRecordLink"].(string), db),
|
||||
"PSBRecord": getSettingValue("psb_record", defaultData["PSBRecord"].(string), db),
|
||||
"PSBRecordLink": getSettingValue("psb_record_link", defaultData["PSBRecordLink"].(string), db),
|
||||
"title": "主页",
|
||||
}
|
||||
|
||||
if err := utils.RenderTemplate(w, "index.html", data); err != nil {
|
||||
http.Error(w, "页面加载失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "index.html", data)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"networkDev/utils"
|
||||
"sync"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
31
go.mod
31
go.mod
@@ -3,6 +3,7 @@ module networkDev
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -19,19 +20,36 @@ require (
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
@@ -39,11 +57,20 @@ require (
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
||||
79
go.sum
79
go.sum
@@ -4,8 +4,14 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -18,20 +24,40 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -42,18 +68,32 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
|
||||
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/redis/go-redis/v9 v9.13.0 h1:PpmlVykE0ODh8P43U0HqC+2NXHXwG+GUtQyz+MPKGRg=
|
||||
github.com/redis/go-redis/v9 v9.13.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
@@ -79,17 +119,30 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
@@ -104,6 +157,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -112,6 +167,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -119,14 +176,16 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -158,7 +217,11 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
97
middleware/logging.go
Normal file
97
middleware/logging.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"networkDev/utils/logger"
|
||||
)
|
||||
|
||||
// LoggingMiddleware 日志记录中间件结构体
|
||||
// 用于记录HTTP请求的详细信息,包括方法、路径、状态码和响应时间
|
||||
type LoggingMiddleware struct {
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware 创建新的日志记录中间件实例
|
||||
func NewLoggingMiddleware(logger *logger.Logger) *LoggingMiddleware {
|
||||
return &LoggingMiddleware{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handler 返回Gin中间件函数,用于记录HTTP请求日志
|
||||
// 记录格式遵循Apache Common Log Format
|
||||
func (lm *LoggingMiddleware) Handler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 记录开始时间
|
||||
start := time.Now()
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 计算处理时间
|
||||
duration := time.Since(start)
|
||||
|
||||
// 获取客户端IP
|
||||
clientIP := getClientIP(c)
|
||||
|
||||
// 记录日志 - Apache Common Log Format
|
||||
// 使用专门的HTTP日志方法避免User-Agent中的反斜杠被转义
|
||||
lm.logger.LogRequestWithHeaders(
|
||||
c.Request.Method,
|
||||
c.Request.RequestURI,
|
||||
clientIP,
|
||||
c.Writer.Status(),
|
||||
duration,
|
||||
"-", // referer (已废弃)
|
||||
c.Request.UserAgent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// getClientIP 获取客户端真实IP地址
|
||||
// 优先从X-Forwarded-For、X-Real-IP等头部获取,最后使用RemoteAddr
|
||||
func getClientIP(c *gin.Context) string {
|
||||
// 检查X-Forwarded-For头部
|
||||
xForwardedFor := c.GetHeader("X-Forwarded-For")
|
||||
if xForwardedFor != "" {
|
||||
// X-Forwarded-For可能包含多个IP,取第一个
|
||||
ips := strings.Split(xForwardedFor, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 检查X-Real-IP头部
|
||||
xRealIP := c.GetHeader("X-Real-IP")
|
||||
if xRealIP != "" {
|
||||
return xRealIP
|
||||
}
|
||||
|
||||
// 检查X-Forwarded头部
|
||||
xForwarded := c.GetHeader("X-Forwarded")
|
||||
if xForwarded != "" {
|
||||
return xForwarded
|
||||
}
|
||||
|
||||
// 使用RemoteAddr
|
||||
remoteAddr := c.Request.RemoteAddr
|
||||
if strings.Contains(remoteAddr, ":") {
|
||||
// 移除端口号
|
||||
if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 {
|
||||
return remoteAddr[:idx]
|
||||
}
|
||||
}
|
||||
|
||||
return remoteAddr
|
||||
}
|
||||
|
||||
// WrapHandler 创建Gin日志中间件
|
||||
// 使用全局日志记录器创建日志中间件
|
||||
func WrapHandler() gin.HandlerFunc {
|
||||
logger := logger.GetLogger()
|
||||
middleware := NewLoggingMiddleware(logger)
|
||||
return middleware.Handler()
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"networkDev/utils/logger"
|
||||
)
|
||||
|
||||
// LoggingMiddleware HTTP请求日志中间件
|
||||
// 记录每个HTTP请求的详细信息,包括方法、路径、状态码和响应时间
|
||||
type LoggingMiddleware struct {
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware 创建新的日志中间件实例
|
||||
func NewLoggingMiddleware(logger *logger.Logger) *LoggingMiddleware {
|
||||
return &LoggingMiddleware{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// responseWriter 包装http.ResponseWriter以捕获状态码
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
written bool
|
||||
}
|
||||
|
||||
// newResponseWriter 创建新的响应写入器包装器
|
||||
func newResponseWriter(w http.ResponseWriter) *responseWriter {
|
||||
return &responseWriter{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK, // 默认状态码
|
||||
}
|
||||
}
|
||||
|
||||
// WriteHeader 重写WriteHeader方法以捕获状态码
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
if !rw.written {
|
||||
rw.statusCode = code
|
||||
rw.written = true
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
}
|
||||
|
||||
// Write 重写Write方法以确保状态码被设置
|
||||
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||
if !rw.written {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return rw.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// Handler 中间件处理器函数
|
||||
// 包装HTTP处理器以添加请求日志记录功能
|
||||
func (lm *LoggingMiddleware) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// 包装响应写入器以捕获状态码
|
||||
wrapped := newResponseWriter(w)
|
||||
|
||||
// 调用下一个处理器
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
// 计算响应时间
|
||||
duration := time.Since(start)
|
||||
|
||||
// 记录请求日志
|
||||
lm.logger.LogRequestWithHeaders(
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
getClientIP(r),
|
||||
wrapped.statusCode,
|
||||
duration,
|
||||
"-",
|
||||
r.Header.Get("User-Agent"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// getClientIP 获取客户端真实IP地址
|
||||
// 优先从X-Forwarded-For、X-Real-IP等头部获取,最后使用RemoteAddr
|
||||
func getClientIP(r *http.Request) string {
|
||||
// 检查X-Forwarded-For头部
|
||||
xForwardedFor := r.Header.Get("X-Forwarded-For")
|
||||
if xForwardedFor != "" {
|
||||
// X-Forwarded-For可能包含多个IP,取第一个
|
||||
ips := strings.Split(xForwardedFor, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 检查X-Real-IP头部
|
||||
xRealIP := r.Header.Get("X-Real-IP")
|
||||
if xRealIP != "" {
|
||||
return xRealIP
|
||||
}
|
||||
|
||||
// 检查X-Forwarded头部
|
||||
xForwarded := r.Header.Get("X-Forwarded")
|
||||
if xForwarded != "" {
|
||||
return xForwarded
|
||||
}
|
||||
|
||||
// 使用RemoteAddr
|
||||
remoteAddr := r.RemoteAddr
|
||||
if strings.Contains(remoteAddr, ":") {
|
||||
// 移除端口号
|
||||
if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 {
|
||||
return remoteAddr[:idx]
|
||||
}
|
||||
}
|
||||
|
||||
return remoteAddr
|
||||
}
|
||||
|
||||
// WrapHandler 包装HTTP处理器以添加日志记录功能
|
||||
// 使用全局日志记录器创建日志中间件
|
||||
func WrapHandler(handler http.Handler) http.Handler {
|
||||
logger := logger.GetLogger()
|
||||
middleware := NewLoggingMiddleware(logger)
|
||||
return middleware.Handler(handler)
|
||||
}
|
||||
|
||||
// WrapHandlerFunc 包装HTTP处理器函数以添加日志记录功能
|
||||
// 将HandlerFunc转换为Handler并添加日志中间件
|
||||
func WrapHandlerFunc(handlerFunc http.HandlerFunc) http.Handler {
|
||||
return WrapHandler(handlerFunc)
|
||||
}
|
||||
130
server/admin.go
130
server/admin.go
@@ -1,9 +1,10 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
adminctl "networkDev/controllers/admin"
|
||||
"networkDev/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterAdminRoutes 注册管理员后台相关路由
|
||||
@@ -12,91 +13,98 @@ import (
|
||||
// - /admin/dashboard: 管理员仪表盘(示例)
|
||||
// - /admin/fragment/*: 布局内动态片段加载
|
||||
// - /admin/api/settings*: 设置接口(查询/更新)
|
||||
func RegisterAdminRoutes(mux *http.ServeMux) {
|
||||
func RegisterAdminRoutes(router *gin.Engine) {
|
||||
// /admin 根与前缀统一入口:根据是否登录跳转
|
||||
mux.HandleFunc("/admin", adminctl.AdminIndexHandler)
|
||||
mux.HandleFunc("/admin/", adminctl.AdminIndexHandler)
|
||||
router.GET("/admin", adminctl.AdminIndexHandler)
|
||||
router.GET("/admin/", adminctl.AdminIndexHandler)
|
||||
|
||||
// Admin 认证相关路由
|
||||
mux.HandleFunc("/admin/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
adminctl.LoginPageHandler(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost {
|
||||
// 应用CSRF保护
|
||||
utils.RequireCSRFToken(adminctl.LoginHandler)(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
})
|
||||
router.GET("/admin/login", adminctl.LoginPageHandler)
|
||||
router.POST("/admin/login", adminctl.LoginHandler) // CSRF验证在控制器内部处理
|
||||
|
||||
// 退出登录(无需拦截,幂等清理)
|
||||
mux.HandleFunc("/admin/logout", adminctl.LogoutHandler)
|
||||
router.POST("/admin/logout", adminctl.LogoutHandler)
|
||||
|
||||
// 验证码生成路由(无需认证)
|
||||
mux.HandleFunc("/admin/captcha", adminctl.CaptchaHandler)
|
||||
router.GET("/admin/captcha", adminctl.CaptchaHandler)
|
||||
|
||||
// CSRF令牌获取API(无需认证,但需要在登录页面等地方获取)
|
||||
mux.HandleFunc("/admin/api/csrf-token", utils.CSRFTokenHandler)
|
||||
router.GET("/admin/api/csrf-token", func(c *gin.Context) {
|
||||
// 生成新的CSRF令牌
|
||||
token, err := utils.GenerateCSRFToken()
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": "生成CSRF令牌失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置令牌到Cookie和响应头
|
||||
utils.SetCSRFToken(c, token)
|
||||
|
||||
// 返回令牌给前端
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "CSRF令牌生成成功",
|
||||
"csrf_token": token,
|
||||
})
|
||||
})
|
||||
|
||||
// 后台布局页(需要管理员认证)
|
||||
mux.HandleFunc("/admin/layout", adminctl.AdminAuthRequired(adminctl.AdminLayoutHandler))
|
||||
router.GET("/admin/layout", adminctl.AdminAuthRequired(), adminctl.AdminLayoutHandler)
|
||||
|
||||
// 片段路由(需要管理员认证)
|
||||
mux.HandleFunc("/admin/dashboard", adminctl.AdminAuthRequired(adminctl.DashboardFragmentHandler))
|
||||
mux.HandleFunc("/admin/user", adminctl.AdminAuthRequired(adminctl.UserFragmentHandler))
|
||||
mux.HandleFunc("/admin/settings", adminctl.AdminAuthRequired(adminctl.SettingsFragmentHandler))
|
||||
mux.HandleFunc("/admin/apps", adminctl.AdminAuthRequired(adminctl.AppsFragmentHandler))
|
||||
mux.HandleFunc("/admin/apis", adminctl.AdminAuthRequired(adminctl.APIFragmentHandler))
|
||||
mux.HandleFunc("/admin/variables", adminctl.AdminAuthRequired(adminctl.VariableFragmentHandler))
|
||||
router.GET("/admin/dashboard", adminctl.AdminAuthRequired(), adminctl.DashboardFragmentHandler)
|
||||
router.GET("/admin/user", adminctl.AdminAuthRequired(), adminctl.UserFragmentHandler)
|
||||
router.GET("/admin/settings", adminctl.AdminAuthRequired(), adminctl.SettingsFragmentHandler)
|
||||
router.GET("/admin/apps", adminctl.AdminAuthRequired(), adminctl.AppsFragmentHandler)
|
||||
router.GET("/admin/apis", adminctl.AdminAuthRequired(), adminctl.APIFragmentHandler)
|
||||
router.GET("/admin/variables", adminctl.AdminAuthRequired(), adminctl.VariableFragmentHandler)
|
||||
|
||||
// 系统信息API(用于仪表盘定时刷新)
|
||||
mux.HandleFunc("/admin/api/system/info", adminctl.AdminAuthRequired(adminctl.SystemInfoHandler))
|
||||
router.GET("/admin/api/system/info", adminctl.AdminAuthRequired(), adminctl.SystemInfoHandler)
|
||||
|
||||
// 仪表盘统计数据API
|
||||
mux.HandleFunc("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(adminctl.DashboardStatsHandler))
|
||||
router.GET("/admin/api/dashboard/stats", adminctl.AdminAuthRequired(), adminctl.DashboardStatsHandler)
|
||||
|
||||
// 个人资料API
|
||||
mux.HandleFunc("/admin/api/user/profile", adminctl.AdminAuthRequired(adminctl.UserProfileQueryHandler))
|
||||
mux.HandleFunc("/admin/api/user/profile/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserProfileUpdateHandler)))
|
||||
mux.HandleFunc("/admin/api/user/password", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.UserPasswordUpdateHandler)))
|
||||
router.GET("/admin/api/user/profile", adminctl.AdminAuthRequired(), adminctl.UserProfileQueryHandler)
|
||||
router.POST("/admin/api/user/profile/update", adminctl.AdminAuthRequired(), adminctl.UserProfileUpdateHandler)
|
||||
router.POST("/admin/api/user/password", adminctl.AdminAuthRequired(), adminctl.UserPasswordUpdateHandler)
|
||||
|
||||
// 系统设置API
|
||||
mux.HandleFunc("/admin/api/settings", adminctl.AdminAuthRequired(adminctl.SettingsQueryHandler))
|
||||
mux.HandleFunc("/admin/api/settings/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.SettingsUpdateHandler)))
|
||||
router.GET("/admin/api/settings", adminctl.AdminAuthRequired(), adminctl.SettingsQueryHandler)
|
||||
router.POST("/admin/api/settings/update", adminctl.AdminAuthRequired(), adminctl.SettingsUpdateHandler)
|
||||
|
||||
// 应用管理API
|
||||
mux.HandleFunc("/admin/api/apps/list", adminctl.AdminAuthRequired(adminctl.AppsListHandler))
|
||||
mux.HandleFunc("/admin/api/apps/create", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppCreateHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppDeleteHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppsBatchDeleteHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppsBatchUpdateStatusHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/reset_secret", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppResetSecretHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/get_app_data", adminctl.AdminAuthRequired(adminctl.AppGetAppDataHandler))
|
||||
mux.HandleFunc("/admin/api/apps/update_app_data", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateAppDataHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/get_announcement", adminctl.AdminAuthRequired(adminctl.AppGetAnnouncementHandler))
|
||||
mux.HandleFunc("/admin/api/apps/update_announcement", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateAnnouncementHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/get_multi_config", adminctl.AdminAuthRequired(adminctl.AppGetMultiConfigHandler))
|
||||
mux.HandleFunc("/admin/api/apps/update_multi_config", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateMultiConfigHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/get_bind_config", adminctl.AdminAuthRequired(adminctl.AppGetBindConfigHandler))
|
||||
mux.HandleFunc("/admin/api/apps/update_bind_config", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateBindConfigHandler)))
|
||||
mux.HandleFunc("/admin/api/apps/get_register_config", adminctl.AdminAuthRequired(adminctl.AppGetRegisterConfigHandler))
|
||||
mux.HandleFunc("/admin/api/apps/update_register_config", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.AppUpdateRegisterConfigHandler)))
|
||||
router.GET("/admin/api/apps/list", adminctl.AdminAuthRequired(), adminctl.AppsListHandler)
|
||||
router.POST("/admin/api/apps/create", adminctl.AdminAuthRequired(), adminctl.AppCreateHandler)
|
||||
router.POST("/admin/api/apps/update", adminctl.AdminAuthRequired(), adminctl.AppUpdateHandler)
|
||||
router.POST("/admin/api/apps/delete", adminctl.AdminAuthRequired(), adminctl.AppDeleteHandler)
|
||||
router.POST("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(), adminctl.AppsBatchDeleteHandler)
|
||||
router.POST("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(), adminctl.AppsBatchUpdateStatusHandler)
|
||||
router.POST("/admin/api/apps/reset_secret", adminctl.AdminAuthRequired(), adminctl.AppResetSecretHandler)
|
||||
router.GET("/admin/api/apps/get_app_data", adminctl.AdminAuthRequired(), adminctl.AppGetAppDataHandler)
|
||||
router.POST("/admin/api/apps/update_app_data", adminctl.AdminAuthRequired(), adminctl.AppUpdateAppDataHandler)
|
||||
router.GET("/admin/api/apps/get_announcement", adminctl.AdminAuthRequired(), adminctl.AppGetAnnouncementHandler)
|
||||
router.POST("/admin/api/apps/update_announcement", adminctl.AdminAuthRequired(), adminctl.AppUpdateAnnouncementHandler)
|
||||
router.GET("/admin/api/apps/get_multi_config", adminctl.AdminAuthRequired(), adminctl.AppGetMultiConfigHandler)
|
||||
router.POST("/admin/api/apps/update_multi_config", adminctl.AdminAuthRequired(), adminctl.AppUpdateMultiConfigHandler)
|
||||
router.GET("/admin/api/apps/get_bind_config", adminctl.AdminAuthRequired(), adminctl.AppGetBindConfigHandler)
|
||||
router.POST("/admin/api/apps/update_bind_config", adminctl.AdminAuthRequired(), adminctl.AppUpdateBindConfigHandler)
|
||||
router.GET("/admin/api/apps/get_register_config", adminctl.AdminAuthRequired(), adminctl.AppGetRegisterConfigHandler)
|
||||
router.POST("/admin/api/apps/update_register_config", adminctl.AdminAuthRequired(), adminctl.AppUpdateRegisterConfigHandler)
|
||||
|
||||
// API接口管理API
|
||||
mux.HandleFunc("/admin/api/apis/list", adminctl.AdminAuthRequired(adminctl.APIListHandler))
|
||||
mux.HandleFunc("/admin/api/apis/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.APIUpdateHandler)))
|
||||
mux.HandleFunc("/admin/api/apis/apps", adminctl.AdminAuthRequired(adminctl.APIGetAppsHandler))
|
||||
mux.HandleFunc("/admin/api/apis/types", adminctl.AdminAuthRequired(adminctl.APIGetTypesHandler))
|
||||
mux.HandleFunc("/admin/api/apis/generate_keys", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.APIGenerateKeysHandler)))
|
||||
router.GET("/admin/api/apis/list", adminctl.AdminAuthRequired(), adminctl.APIListHandler)
|
||||
router.POST("/admin/api/apis/update", adminctl.AdminAuthRequired(), adminctl.APIUpdateHandler)
|
||||
router.GET("/admin/api/apis/apps", adminctl.AdminAuthRequired(), adminctl.APIGetAppsHandler)
|
||||
router.GET("/admin/api/apis/types", adminctl.AdminAuthRequired(), adminctl.APIGetTypesHandler)
|
||||
router.POST("/admin/api/apis/generate_keys", adminctl.AdminAuthRequired(), adminctl.APIGenerateKeysHandler)
|
||||
|
||||
// 变量管理API
|
||||
mux.HandleFunc("/admin/variable/list", adminctl.AdminAuthRequired(adminctl.VariableListHandler))
|
||||
mux.HandleFunc("/admin/variable/apps", adminctl.AdminAuthRequired(adminctl.VariableGetAppsHandler))
|
||||
mux.HandleFunc("/admin/variable/create", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableCreateHandler)))
|
||||
mux.HandleFunc("/admin/variable/update", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableUpdateHandler)))
|
||||
mux.HandleFunc("/admin/variable/delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariableDeleteHandler)))
|
||||
mux.HandleFunc("/admin/variable/batch_delete", adminctl.AdminAuthRequired(utils.RequireCSRFToken(adminctl.VariablesBatchDeleteHandler)))
|
||||
router.GET("/admin/variable/list", adminctl.AdminAuthRequired(), adminctl.VariableListHandler)
|
||||
router.GET("/admin/variable/apps", adminctl.AdminAuthRequired(), adminctl.VariableGetAppsHandler)
|
||||
router.POST("/admin/variable/create", adminctl.AdminAuthRequired(), adminctl.VariableCreateHandler)
|
||||
router.POST("/admin/variable/update", adminctl.AdminAuthRequired(), adminctl.VariableUpdateHandler)
|
||||
router.POST("/admin/variable/delete", adminctl.AdminAuthRequired(), adminctl.VariableDeleteHandler)
|
||||
router.POST("/admin/variable/batch_delete", adminctl.AdminAuthRequired(), adminctl.VariablesBatchDeleteHandler)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/controllers/home"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterHomeRoutes 注册主页路由
|
||||
// 只包含根路径,用于主页功能
|
||||
func RegisterHomeRoutes(mux *http.ServeMux) {
|
||||
func RegisterHomeRoutes(router *gin.Engine) {
|
||||
// 根路径 - 主页
|
||||
mux.HandleFunc("/", home.RootHandler)
|
||||
router.GET("/", home.RootHandler)
|
||||
}
|
||||
|
||||
@@ -5,30 +5,32 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"networkDev/web"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterRoutes 聚合注册所有路由
|
||||
func RegisterRoutes(mux *http.ServeMux) {
|
||||
registerStaticRoutes(mux)
|
||||
registerFaviconRoute(mux)
|
||||
RegisterHomeRoutes(mux)
|
||||
RegisterAdminRoutes(mux)
|
||||
func RegisterRoutes(router *gin.Engine) {
|
||||
registerStaticRoutes(router)
|
||||
registerFaviconRoute(router)
|
||||
RegisterHomeRoutes(router)
|
||||
RegisterAdminRoutes(router)
|
||||
|
||||
}
|
||||
|
||||
// registerStaticRoutes 注册静态资源路由
|
||||
// 静态资源服务,将 /static/ 和 /assets/ 映射到嵌入的文件系统
|
||||
func registerStaticRoutes(mux *http.ServeMux) {
|
||||
func registerStaticRoutes(router *gin.Engine) {
|
||||
if fsys, err := web.GetStaticFS(); err == nil {
|
||||
// 为 /static/ 路径创建子文件系统
|
||||
if staticSubFS, staticErr := fs.Sub(fsys, "static"); staticErr == nil {
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSubFS))))
|
||||
router.StaticFS("/static", http.FS(staticSubFS))
|
||||
} else {
|
||||
log.Printf("创建静态资源子文件系统失败: %v", staticErr)
|
||||
}
|
||||
// 为 /assets/ 路径创建子文件系统
|
||||
if assetsSubFS, assetsErr := fs.Sub(fsys, "assets"); assetsErr == nil {
|
||||
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(assetsSubFS))))
|
||||
router.StaticFS("/assets", http.FS(assetsSubFS))
|
||||
} else {
|
||||
log.Printf("创建资产资源子文件系统失败: %v", assetsErr)
|
||||
}
|
||||
@@ -38,9 +40,9 @@ func registerStaticRoutes(mux *http.ServeMux) {
|
||||
}
|
||||
|
||||
// registerFaviconRoute 注册favicon路由
|
||||
func registerFaviconRoute(mux *http.ServeMux) {
|
||||
func registerFaviconRoute(router *gin.Engine) {
|
||||
// 将 /favicon.ico 重定向到 /assets/favicon.svg
|
||||
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/assets/favicon.svg", http.StatusMovedPermanently)
|
||||
router.GET("/favicon.ico", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusMovedPermanently, "/assets/favicon.svg")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// FindSettingByName 根据名称查找设置
|
||||
// name: 设置名称
|
||||
// db: 数据库连接
|
||||
@@ -30,8 +26,6 @@ func FindSettingByName(name string, db *gorm.DB) (*models.Settings, error) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// UpdateEntityByID 根据ID更新实体
|
||||
// model: 模型类型
|
||||
// id: 实体ID
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/web"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JsonResponse 通用JSON响应函数
|
||||
// 将 success 转换为 code:true -> 0, false -> 1,并输出 data
|
||||
func JsonResponse(w http.ResponseWriter, status int, success bool, message string, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
// 将success转换为code格式:true -> 0, false -> 1
|
||||
code := 1
|
||||
if success {
|
||||
code = 0
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": code,
|
||||
"msg": message,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// RenderTemplate 通用模板渲染函数
|
||||
// templateName: 模板文件名
|
||||
// data: 模板数据
|
||||
// w: HTTP响应写入器
|
||||
func RenderTemplate(w http.ResponseWriter, templateName string, data map[string]interface{}) error {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
tmpl, err := web.ParseTemplates()
|
||||
if err != nil {
|
||||
http.Error(w, "模板解析失败", http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, templateName, data); err != nil {
|
||||
http.Error(w, "模板渲染失败", http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultTemplateData 获取默认模板数据
|
||||
// 返回包含系统基础信息的数据映射
|
||||
func GetDefaultTemplateData() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"SystemName": "网络验证系统",
|
||||
"FooterText": "© 2025 凌动技术 保留所有权利",
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplateDataWithCSRF 获取包含CSRF令牌的模板数据
|
||||
// 合并默认数据和CSRF令牌,用于需要CSRF保护的页面
|
||||
func GetTemplateDataWithCSRF(r *http.Request, additionalData map[string]interface{}) map[string]interface{} {
|
||||
// 获取默认模板数据
|
||||
data := GetDefaultTemplateData()
|
||||
|
||||
// 添加CSRF令牌
|
||||
data["CSRFToken"] = GetCSRFTokenForTemplate(r)
|
||||
|
||||
// 合并额外数据
|
||||
for key, value := range additionalData {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// GetClientIP 获取客户端IP地址
|
||||
// 优先从 X-Forwarded-For 和 X-Real-IP 头部获取,否则使用 RemoteAddr
|
||||
func GetClientIP(r *http.Request) string {
|
||||
// 检查 X-Forwarded-For 头部
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// X-Forwarded-For 可能包含多个IP,取第一个
|
||||
if idx := strings.Index(xff, ","); idx != -1 {
|
||||
return strings.TrimSpace(xff[:idx])
|
||||
}
|
||||
return strings.TrimSpace(xff)
|
||||
}
|
||||
|
||||
// 检查 X-Real-IP 头部
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return strings.TrimSpace(xri)
|
||||
}
|
||||
|
||||
// 使用 RemoteAddr
|
||||
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
|
||||
return r.RemoteAddr[:idx]
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
@@ -121,15 +121,6 @@ func DecryptString(enc string) (string, error) {
|
||||
return string(plain), nil
|
||||
}
|
||||
|
||||
// ResetCrypto 重置加密管理器(用于配置更新后重新初始化)
|
||||
func ResetCrypto() {
|
||||
cryptoManager.mutex.Lock()
|
||||
defer cryptoManager.mutex.Unlock()
|
||||
cryptoManager.inited = false
|
||||
cryptoManager.key = nil
|
||||
cryptoManager.gcm = nil
|
||||
}
|
||||
|
||||
// EncryptStringBatch 批量加密字符串
|
||||
// 减少锁竞争,提高批量处理性能
|
||||
func EncryptStringBatch(plains []string) ([]string, error) {
|
||||
@@ -281,12 +272,12 @@ func DecryptStringWithSalt(enc, salt string) (string, error) {
|
||||
if len(combined) < len(salt) {
|
||||
return "", errors.New("decrypted data too short")
|
||||
}
|
||||
|
||||
|
||||
// 验证盐值是否匹配
|
||||
if combined[len(combined)-len(salt):] != salt {
|
||||
return "", errors.New("salt mismatch")
|
||||
}
|
||||
|
||||
|
||||
return combined[:len(combined)-len(salt)], nil
|
||||
}
|
||||
|
||||
|
||||
109
utils/csrf.go
109
utils/csrf.go
@@ -5,6 +5,8 @@ import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,55 +36,51 @@ func GenerateCSRFToken() (string, error) {
|
||||
}
|
||||
|
||||
// SetCSRFToken 设置CSRF令牌到Cookie和响应头
|
||||
func SetCSRFToken(w http.ResponseWriter, token string) {
|
||||
// 设置CSRF令牌到Cookie
|
||||
cookie := CreateSecureCookie(CSRFCookieName, token, 3600) // 1小时过期
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
// 设置CSRF令牌到响应头,方便JavaScript获取
|
||||
w.Header().Set("X-CSRF-Token", token)
|
||||
func SetCSRFToken(c *gin.Context, token string) {
|
||||
c.SetCookie(CSRFCookieName, token, 3600*24, "/", "", false, true)
|
||||
c.Header(CSRFHeaderName, token)
|
||||
}
|
||||
|
||||
// GetCSRFTokenFromRequest 从请求中获取CSRF令牌
|
||||
// GetCSRFTokenFromRequest 从Gin请求中获取CSRF令牌
|
||||
// 优先级:Header > Form > Cookie
|
||||
func GetCSRFTokenFromRequest(r *http.Request) string {
|
||||
func GetCSRFTokenFromRequest(c *gin.Context) string {
|
||||
// 1. 从Header获取
|
||||
if token := r.Header.Get(CSRFHeaderName); token != "" {
|
||||
if token := c.GetHeader(CSRFHeaderName); token != "" {
|
||||
return token
|
||||
}
|
||||
|
||||
// 2. 从Form获取
|
||||
if token := r.FormValue(CSRFFormField); token != "" {
|
||||
if token := c.PostForm(CSRFFormField); token != "" {
|
||||
return token
|
||||
}
|
||||
|
||||
// 3. 从Cookie获取(作为备选)
|
||||
if cookie, err := r.Cookie(CSRFCookieName); err == nil {
|
||||
return cookie.Value
|
||||
if cookie, err := c.Cookie(CSRFCookieName); err == nil {
|
||||
return cookie
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetCSRFTokenFromCookie 从Cookie中获取CSRF令牌
|
||||
func GetCSRFTokenFromCookie(r *http.Request) string {
|
||||
cookie, err := r.Cookie(CSRFCookieName)
|
||||
func GetCSRFTokenFromCookie(c *gin.Context) string {
|
||||
cookie, err := c.Cookie(CSRFCookieName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return cookie.Value
|
||||
return cookie
|
||||
}
|
||||
|
||||
// ValidateCSRFToken 验证CSRF令牌
|
||||
func ValidateCSRFToken(r *http.Request) bool {
|
||||
func ValidateCSRFToken(c *gin.Context) bool {
|
||||
// 获取Cookie中的令牌(服务器端存储的)
|
||||
cookieToken := GetCSRFTokenFromCookie(r)
|
||||
cookieToken := GetCSRFTokenFromCookie(c)
|
||||
if cookieToken == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取请求中的令牌(客户端提交的)
|
||||
requestToken := GetCSRFTokenFromRequest(r)
|
||||
requestToken := GetCSRFTokenFromRequest(c)
|
||||
if requestToken == "" {
|
||||
return false
|
||||
}
|
||||
@@ -92,47 +90,62 @@ func ValidateCSRFToken(r *http.Request) bool {
|
||||
}
|
||||
|
||||
// CSRFProtection CSRF保护中间件
|
||||
func CSRFProtection(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
func CSRFProtection() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 对于GET、HEAD、OPTIONS请求,只生成令牌,不验证
|
||||
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
|
||||
if c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead || c.Request.Method == http.MethodOptions {
|
||||
// 生成新的CSRF令牌
|
||||
token, err := GenerateCSRFToken()
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "Internal Server Error",
|
||||
"data": nil,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
SetCSRFToken(w, token)
|
||||
next(w, r)
|
||||
SetCSRFToken(c, token)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 对于POST、PUT、DELETE等修改性请求,验证CSRF令牌
|
||||
if !ValidateCSRFToken(r) {
|
||||
JsonResponse(w, http.StatusForbidden, false, "CSRF令牌验证失败", nil)
|
||||
if !ValidateCSRFToken(c) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": 1,
|
||||
"msg": "CSRF令牌验证失败",
|
||||
"data": nil,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证通过,继续处理请求
|
||||
next(w, r)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireCSRFToken 要求CSRF令牌的中间件(用于特定路由)
|
||||
func RequireCSRFToken(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !ValidateCSRFToken(r) {
|
||||
JsonResponse(w, http.StatusForbidden, false, "CSRF令牌验证失败", nil)
|
||||
func RequireCSRFToken() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !ValidateCSRFToken(c) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": 1,
|
||||
"msg": "CSRF令牌验证失败",
|
||||
"data": nil,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetCSRFTokenForTemplate 获取用于模板的CSRF令牌
|
||||
func GetCSRFTokenForTemplate(r *http.Request) string {
|
||||
func GetCSRFTokenForTemplate(c *gin.Context) string {
|
||||
// 尝试从Cookie获取现有令牌
|
||||
if token := GetCSRFTokenFromCookie(r); token != "" {
|
||||
if token := GetCSRFTokenFromCookie(c); token != "" {
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -145,24 +158,36 @@ func GetCSRFTokenForTemplate(r *http.Request) string {
|
||||
}
|
||||
|
||||
// CSRFTokenHandler 专门用于获取CSRF令牌的API端点
|
||||
func CSRFTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
JsonResponse(w, http.StatusMethodNotAllowed, false, "只支持GET请求", nil)
|
||||
func CSRFTokenHandler(c *gin.Context) {
|
||||
if c.Request.Method != http.MethodGet {
|
||||
c.JSON(http.StatusMethodNotAllowed, gin.H{
|
||||
"code": 1,
|
||||
"msg": "只支持GET请求",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新的CSRF令牌
|
||||
token, err := GenerateCSRFToken()
|
||||
if err != nil {
|
||||
JsonResponse(w, http.StatusInternalServerError, false, "生成CSRF令牌失败", nil)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 1,
|
||||
"msg": "生成CSRF令牌失败",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置令牌到Cookie和响应头
|
||||
SetCSRFToken(w, token)
|
||||
SetCSRFToken(c, token)
|
||||
|
||||
// 返回令牌给前端
|
||||
JsonResponse(w, http.StatusOK, true, "CSRF令牌获取成功", map[string]interface{}{
|
||||
"csrf_token": token,
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "CSRF令牌生成成功",
|
||||
"data": gin.H{
|
||||
"csrf_token": token,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -68,27 +68,13 @@ type LogEntry struct {
|
||||
Line int `json:"line"` // 源文件行号
|
||||
}
|
||||
|
||||
// WriteJSONResponse 写入JSON响应
|
||||
// w: HTTP响应写入器
|
||||
// statusCode: HTTP状态码
|
||||
// response: 响应数据
|
||||
func WriteJSONResponse(w http.ResponseWriter, statusCode int, response interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
LogError("Failed to encode JSON response", err, nil)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteErrorResponse 写入错误响应
|
||||
// w: HTTP响应写入器
|
||||
// c: Gin上下文
|
||||
// statusCode: HTTP状态码
|
||||
// message: 错误消息
|
||||
// errorCode: 错误代码
|
||||
// data: 附加数据
|
||||
func WriteErrorResponse(w http.ResponseWriter, statusCode int, message, errorCode string, data interface{}) {
|
||||
func WriteErrorResponse(c *gin.Context, statusCode int, message, errorCode string, data interface{}) {
|
||||
response := ErrorResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
@@ -97,15 +83,15 @@ func WriteErrorResponse(w http.ResponseWriter, statusCode int, message, errorCod
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
WriteJSONResponse(w, statusCode, response)
|
||||
c.JSON(statusCode, response)
|
||||
}
|
||||
|
||||
// WriteSuccessResponse 写入成功响应
|
||||
// w: HTTP响应写入器
|
||||
// c: Gin上下文
|
||||
// statusCode: HTTP状态码
|
||||
// message: 成功消息
|
||||
// data: 响应数据
|
||||
func WriteSuccessResponse(w http.ResponseWriter, statusCode int, message string, data interface{}) {
|
||||
func WriteSuccessResponse(c *gin.Context, statusCode int, message string, data interface{}) {
|
||||
response := SuccessResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
@@ -113,57 +99,57 @@ func WriteSuccessResponse(w http.ResponseWriter, statusCode int, message string,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
WriteJSONResponse(w, statusCode, response)
|
||||
c.JSON(statusCode, response)
|
||||
}
|
||||
|
||||
// HandleDatabaseError 处理数据库错误
|
||||
// w: HTTP响应写入器
|
||||
// c: Gin上下文
|
||||
// err: 数据库错误
|
||||
// operation: 操作描述
|
||||
func HandleDatabaseError(w http.ResponseWriter, err error, operation string) {
|
||||
func HandleDatabaseError(c *gin.Context, err error, operation string) {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
LogWarn(fmt.Sprintf("Record not found during %s", operation), map[string]interface{}{
|
||||
"operation": operation,
|
||||
"error": err.Error(),
|
||||
})
|
||||
WriteErrorResponse(w, http.StatusNotFound, "记录不存在", ErrCodeNotFound, nil)
|
||||
WriteErrorResponse(c, 404, "记录不存在", ErrCodeNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
LogError(fmt.Sprintf("Database error during %s", operation), err, map[string]interface{}{
|
||||
"operation": operation,
|
||||
})
|
||||
WriteErrorResponse(w, http.StatusInternalServerError, "数据库操作失败", ErrCodeDatabaseError, nil)
|
||||
WriteErrorResponse(c, 500, "数据库操作失败", ErrCodeDatabaseError, nil)
|
||||
}
|
||||
|
||||
// HandleValidationError 处理验证错误
|
||||
// w: HTTP响应写入器
|
||||
// c: Gin上下文
|
||||
// message: 验证错误消息
|
||||
// details: 验证错误详情
|
||||
func HandleValidationError(w http.ResponseWriter, message string, details interface{}) {
|
||||
func HandleValidationError(c *gin.Context, message string, details interface{}) {
|
||||
LogWarn("Validation error: "+message, map[string]interface{}{
|
||||
"details": details,
|
||||
})
|
||||
WriteErrorResponse(w, http.StatusBadRequest, message, ErrCodeValidationError, details)
|
||||
WriteErrorResponse(c, 400, message, ErrCodeValidationError, details)
|
||||
}
|
||||
|
||||
// HandleUnauthorizedError 处理未授权错误
|
||||
// w: HTTP响应写入器
|
||||
// c: Gin上下文
|
||||
// message: 错误消息
|
||||
func HandleUnauthorizedError(w http.ResponseWriter, message string) {
|
||||
func HandleUnauthorizedError(c *gin.Context, message string) {
|
||||
LogWarn("Unauthorized access: "+message, nil)
|
||||
WriteErrorResponse(w, http.StatusUnauthorized, message, ErrCodeUnauthorized, nil)
|
||||
WriteErrorResponse(c, 401, message, ErrCodeUnauthorized, nil)
|
||||
}
|
||||
|
||||
// HandleInternalError 处理内部错误
|
||||
// w: HTTP响应写入器
|
||||
// c: Gin上下文
|
||||
// err: 错误
|
||||
// operation: 操作描述
|
||||
func HandleInternalError(w http.ResponseWriter, err error, operation string) {
|
||||
func HandleInternalError(c *gin.Context, err error, operation string) {
|
||||
LogError(fmt.Sprintf("Internal error during %s", operation), err, map[string]interface{}{
|
||||
"operation": operation,
|
||||
})
|
||||
WriteErrorResponse(w, http.StatusInternalServerError, "服务器内部错误", ErrCodeInternalError, nil)
|
||||
WriteErrorResponse(c, 500, "服务器内部错误", ErrCodeInternalError, nil)
|
||||
}
|
||||
|
||||
// LogInfo 记录信息日志
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
url: '/admin/api/apis/apps',
|
||||
type: 'GET',
|
||||
success: function (res) {
|
||||
if (res.success && res.data) {
|
||||
if (res.code === 0 && res.data) {
|
||||
var filterSelect = $('select[name="app_uuid"]').eq(0);
|
||||
// 清空现有选项(保留默认选项)
|
||||
filterSelect.find('option:not(:first)').remove();
|
||||
@@ -303,7 +303,7 @@
|
||||
url: '/admin/api/apis/types',
|
||||
type: 'GET',
|
||||
success: function (res) {
|
||||
if (res.success && res.data) {
|
||||
if (res.code === 0 && res.data) {
|
||||
var typeSelect = $('select[name="api_type"]').eq(0);
|
||||
// 清空现有选项(保留默认选项)
|
||||
typeSelect.find('option:not(:first)').remove();
|
||||
@@ -453,7 +453,7 @@
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ side: side, algorithm: algorithm }),
|
||||
success: function (res) {
|
||||
if (res.success && res.data) {
|
||||
if (res.code === 0 && res.data) {
|
||||
if (algorithm === 1) { // RC4
|
||||
$('[name="' + prefix + '_private_key"]').val(res.data.private_key || '');
|
||||
} else if (algorithm === 4) { // 易加密
|
||||
@@ -464,7 +464,7 @@
|
||||
}
|
||||
layer.msg('已自动生成' + (side === 'submit' ? '提交' : '返回') + '密钥', { icon: 1, time: 1500 });
|
||||
} else {
|
||||
layer.msg(res.message || '自动生成密钥失败', { icon: 2 });
|
||||
layer.msg(res.msg || '自动生成密钥失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
@@ -498,7 +498,7 @@
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ side: 'submit', algorithm: algo }),
|
||||
success: function (res) {
|
||||
if (res.success && res.data) {
|
||||
if (res.code === 0 && res.data) {
|
||||
if (algo === 1) { // RC4
|
||||
$('[name="submit_private_key"]').val(res.data.private_key || '');
|
||||
} else if (algo === 4) { // 易加密
|
||||
@@ -508,7 +508,7 @@
|
||||
$('[name="submit_private_key"]').val(res.data.private_key || '');
|
||||
}
|
||||
} else {
|
||||
layer.msg(res.message || '生成失败', { icon: 2 });
|
||||
layer.msg(res.msg || '生成失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function () { layer.msg('生成失败', { icon: 2 }); }
|
||||
@@ -528,7 +528,7 @@
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ side: 'return', algorithm: algo }),
|
||||
success: function (res) {
|
||||
if (res.success && res.data) {
|
||||
if (res.code === 0 && res.data) {
|
||||
if (algo === 1) { // RC4
|
||||
$('[name="return_private_key"]').val(res.data.private_key || '');
|
||||
} else if (algo === 4) { // 易加密
|
||||
@@ -538,7 +538,7 @@
|
||||
$('[name="return_private_key"]').val(res.data.private_key || '');
|
||||
}
|
||||
} else {
|
||||
layer.msg(res.message || '生成失败', { icon: 2 });
|
||||
layer.msg(res.msg || '生成失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function () { layer.msg('生成失败', { icon: 2 }); }
|
||||
@@ -602,12 +602,12 @@
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function (res) {
|
||||
if (res.success) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('接口更新成功', { icon: 1 });
|
||||
layer.close(index);
|
||||
apisTable.reload();
|
||||
} else {
|
||||
layer.msg(res.message || '更新失败', { icon: 2 });
|
||||
layer.msg(res.msg || '更新失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
|
||||
@@ -761,15 +761,17 @@
|
||||
$.ajax({
|
||||
url: '/admin/api/apps/get_multi_config?uuid=' + obj.data.uuid,
|
||||
type: 'GET',
|
||||
success: function (config) {
|
||||
// 填充表单数据
|
||||
$('input[name="login_type"][value="' + config.login_type + '"]').prop('checked', true);
|
||||
$('input[name="multi_open_scope"][value="' + config.multi_open_scope + '"]').prop('checked', true);
|
||||
$('input[name="clean_interval"]').val(config.clean_interval);
|
||||
$('input[name="check_interval"]').val(config.check_interval);
|
||||
$('input[name="multi_open_count"]').val(config.multi_open_count);
|
||||
success: function (res) {
|
||||
if (res.code === 0 && res.data) {
|
||||
var config = res.data;
|
||||
// 填充表单数据
|
||||
$('input[name="login_type"][value="' + config.login_type + '"]').prop('checked', true);
|
||||
$('input[name="multi_open_scope"][value="' + config.multi_open_scope + '"]').prop('checked', true);
|
||||
$('input[name="clean_interval"]').val(config.clean_interval);
|
||||
$('input[name="check_interval"]').val(config.check_interval);
|
||||
$('input[name="multi_open_count"]').val(config.multi_open_count);
|
||||
|
||||
// 打开静态弹窗
|
||||
// 打开静态弹窗
|
||||
var multiConfigIndex = layer.open({
|
||||
type: 1,
|
||||
title: '多开配置 - ' + obj.data.name,
|
||||
@@ -815,7 +817,7 @@
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function (res) {
|
||||
if (res.message) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('多开配置更新成功', { icon: 1 });
|
||||
layer.close(index);
|
||||
table.reload('appsTable');
|
||||
@@ -836,6 +838,9 @@
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg || '获取多开配置失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
layer.msg('获取多开配置失败,请稍后重试', { icon: 2 });
|
||||
@@ -884,9 +889,11 @@
|
||||
$.ajax({
|
||||
url: '/admin/api/apps/get_bind_config?uuid=' + obj.data.uuid,
|
||||
type: 'GET',
|
||||
success: function (config) {
|
||||
// 填充表单数据
|
||||
$('#bindConfigModal input[name="machine_verify"][value="' + config.machine_verify + '"]').prop('checked', true);
|
||||
success: function (res) {
|
||||
if (res.code === 0 && res.data) {
|
||||
var config = res.data;
|
||||
// 填充表单数据
|
||||
$('#bindConfigModal input[name="machine_verify"][value="' + config.machine_verify + '"]').prop('checked', true);
|
||||
$('#bindConfigModal input[name="machine_rebind_enabled"][value="' + config.machine_rebind_enabled + '"]').prop('checked', true);
|
||||
$('#bindConfigModal input[name="machine_rebind_limit"][value="' + config.machine_rebind_limit + '"]').prop('checked', true);
|
||||
$('#bindConfigModal input[name="machine_free_count"]').val(config.machine_free_count);
|
||||
@@ -977,6 +984,9 @@
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg || '获取绑定设置失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
layer.msg('获取绑定设置失败,请稍后重试', { icon: 2 });
|
||||
@@ -987,9 +997,11 @@
|
||||
$.ajax({
|
||||
url: '/admin/api/apps/get_register_config?uuid=' + obj.data.uuid,
|
||||
type: 'GET',
|
||||
success: function (config) {
|
||||
// 填充表单数据
|
||||
$('#registerConfigModal input[name="register_enabled"][value="' + config.register_enabled + '"]').prop('checked', true);
|
||||
success: function (res) {
|
||||
if (res.code === 0 && res.data) {
|
||||
var config = res.data;
|
||||
// 填充表单数据
|
||||
$('#registerConfigModal input[name="register_enabled"][value="' + config.register_enabled + '"]').prop('checked', true);
|
||||
$('#registerConfigModal input[name="register_limit_enabled"][value="' + config.register_limit_enabled + '"]').prop('checked', true);
|
||||
$('#registerConfigModal input[name="register_limit_time"][value="' + config.register_limit_time + '"]').prop('checked', true);
|
||||
$('#registerConfigModal input[name="register_count"]').val(config.register_count);
|
||||
@@ -1074,6 +1086,9 @@
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg || '获取注册设置失败', { icon: 2 });
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
layer.msg('获取注册设置失败,请稍后重试', { icon: 2 });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ define "variables" }}
|
||||
{{ define "variables.html" }}
|
||||
<section>
|
||||
<h2>变量管理</h2>
|
||||
<div class="layui-btn-container" style="margin:12px 0">
|
||||
|
||||
Submodule web/template/layui-theme-dark deleted from a89e6787f4
Reference in New Issue
Block a user