- 新增 crypto.go: AES-256-GCM 加解密,与 Python backend 兼容 - auth.go: Login 添加 LDAP fallback(本地优先→LDAP绑定→自动创建本地用户) - ldap.go: 实现真实 LDAP 连接/用户搜索/密码验证/OU列表/用户列表 - 所有 LDAP 操作解密 admin_password 后使用 - 新增/更新密码存储时加密 - router.go: 补充 delete-user 路由 - 添加 go-ldap/v3 依赖
198 lines
5.5 KiB
Go
198 lines
5.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"sql-platform/internal/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ChannelResponse — config as JSON object for frontend
|
|
type ChannelResponse struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
ChannelType string `json:"channel_type"`
|
|
Config map[string]interface{} `json:"config"`
|
|
IsEnabled bool `json:"is_enabled"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
func toChannelResponse(ch model.NotificationChannel) ChannelResponse {
|
|
var cfg map[string]interface{}
|
|
if ch.Config != "" {
|
|
_ = json.Unmarshal([]byte(ch.Config), &cfg)
|
|
}
|
|
if cfg == nil {
|
|
cfg = map[string]interface{}{}
|
|
}
|
|
enabled := ch.IsEnabled != nil && *ch.IsEnabled
|
|
return ChannelResponse{
|
|
ID: ch.ID,
|
|
Name: ch.Name,
|
|
ChannelType: ch.ChannelType,
|
|
Config: cfg,
|
|
IsEnabled: enabled,
|
|
CreatedAt: ch.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
|
UpdatedAt: ch.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
|
}
|
|
}
|
|
|
|
// GET /api/v1/notifications
|
|
func ListNotifications(c *gin.Context) {
|
|
var channels []model.NotificationChannel
|
|
model.DB.Order("id").Find(&channels)
|
|
result := make([]ChannelResponse, 0, len(channels))
|
|
for _, ch := range channels {
|
|
result = append(result, toChannelResponse(ch))
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// POST /api/v1/notifications
|
|
func CreateNotification(c *gin.Context) {
|
|
var input struct {
|
|
Name string `json:"name"`
|
|
ChannelType string `json:"channel_type"`
|
|
Config map[string]interface{} `json:"config"`
|
|
IsEnabled bool `json:"is_enabled"`
|
|
}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "参数错误"})
|
|
return
|
|
}
|
|
if input.Name == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "名称不能为空"})
|
|
return
|
|
}
|
|
cfgBytes, _ := json.Marshal(input.Config)
|
|
enabled := input.IsEnabled
|
|
ch := model.NotificationChannel{
|
|
Name: input.Name,
|
|
ChannelType: input.ChannelType,
|
|
Config: string(cfgBytes),
|
|
IsEnabled: &enabled,
|
|
}
|
|
if err := model.DB.Create(&ch).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"detail": "创建失败"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toChannelResponse(ch))
|
|
}
|
|
|
|
// PUT /api/v1/notifications/:id
|
|
func UpdateNotification(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
var ch model.NotificationChannel
|
|
if err := model.DB.First(&ch, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"detail": "通知渠道不存在"})
|
|
return
|
|
}
|
|
var input map[string]interface{}
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "参数错误"})
|
|
return
|
|
}
|
|
if name, ok := input["name"].(string); ok {
|
|
ch.Name = name
|
|
}
|
|
if ct, ok := input["channel_type"].(string); ok {
|
|
ch.ChannelType = ct
|
|
}
|
|
if cfg, ok := input["config"].(map[string]interface{}); ok {
|
|
cfgBytes, _ := json.Marshal(cfg)
|
|
ch.Config = string(cfgBytes)
|
|
}
|
|
if enabled, ok := input["is_enabled"].(bool); ok {
|
|
ch.IsEnabled = &enabled
|
|
}
|
|
model.DB.Save(&ch)
|
|
c.JSON(http.StatusOK, toChannelResponse(ch))
|
|
}
|
|
|
|
// DELETE /api/v1/notifications/:id
|
|
func DeleteNotification(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
model.DB.Delete(&model.NotificationChannel{}, id)
|
|
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
|
|
}
|
|
|
|
// POST /api/v1/notifications/:id/test
|
|
func TestNotificationSend(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
var ch model.NotificationChannel
|
|
if err := model.DB.First(&ch, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"detail": "通知渠道不存在"})
|
|
return
|
|
}
|
|
|
|
var cfg map[string]interface{}
|
|
_ = json.Unmarshal([]byte(ch.Config), &cfg)
|
|
webhookURL, _ := cfg["webhook_url"].(string)
|
|
if webhookURL == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "Webhook URL 未配置"})
|
|
return
|
|
}
|
|
|
|
title := "SQL审核平台 - 通知测试"
|
|
body := "这是一条测试消息,如果收到说明通知渠道配置正确。"
|
|
|
|
var sendErr error
|
|
switch ch.ChannelType {
|
|
case "webhook_feishu":
|
|
sendErr = sendFeishuWebhook(webhookURL, title, body)
|
|
case "dingtalk":
|
|
secret, _ := cfg["secret"].(string)
|
|
sendErr = sendDingtalkWebhook(webhookURL, secret, title, body)
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "不支持的通知类型: " + ch.ChannelType})
|
|
return
|
|
}
|
|
|
|
if sendErr != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"detail": "发送失败: " + sendErr.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "测试消息已发送"})
|
|
}
|
|
|
|
func sendFeishuWebhook(webhookURL, title, body string) error {
|
|
payload := map[string]interface{}{
|
|
"msg_type": "interactive",
|
|
"card": map[string]interface{}{
|
|
"elements": []map[string]interface{}{
|
|
{"tag": "markdown", "content": body},
|
|
},
|
|
},
|
|
}
|
|
return postJSON(webhookURL, payload)
|
|
}
|
|
|
|
func sendDingtalkWebhook(webhookURL, secret, title, body string) error {
|
|
dingBody := title + "\n\n" + body
|
|
payload := map[string]interface{}{
|
|
"msgtype": "markdown",
|
|
"markdown": map[string]string{
|
|
"title": title,
|
|
"text": dingBody,
|
|
},
|
|
}
|
|
return postJSON(webhookURL, payload)
|
|
}
|
|
|
|
func postJSON(url string, payload interface{}) error {
|
|
data, _ := json.Marshal(payload)
|
|
resp, err := http.Post(url, "application/json", bytes.NewReader(data))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
return nil
|
|
}
|