Files
nezha_domains/cmd/dashboard/controller/notification.go
T
naiba 39d398066d fix(api): redact third-party credentials in ddns/notification list
GET /api/v1/ddns and /api/v1/notification returned full objects with
plaintext credentials (Cloudflare/TencentCloud secrets, webhook URLs
with embedded bot tokens, Authorization headers). Redact these fields
in the list responses.

Since the frontend edit form repopulates from the list endpoint, the
update handlers now treat an empty submitted credential as "no change"
and preserve the stored value, preventing accidental secret wipes.

Ref: GHSA-ww5p-j6cj-6mqq
2026-06-20 01:54:50 +00:00

202 lines
5.1 KiB
Go

package controller
import (
"slices"
"strconv"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"gorm.io/gorm"
"github.com/nezhahq/nezha/model"
"github.com/nezhahq/nezha/service/singleton"
)
// List notification
// @Summary List notification
// @Security BearerAuth
// @Schemes
// @Description List notification
// @Tags auth required
// @Param id query uint false "Resource ID"
// @Produce json
// @Success 200 {object} model.CommonResponse[[]model.Notification]
// @Router /notification [get]
func listNotification(c *gin.Context) ([]*model.Notification, error) {
slist := singleton.NotificationShared.GetSortedList()
var notifications []*model.Notification
if err := copier.Copy(&notifications, &slist); err != nil {
return nil, err
}
// 列表端点不回显写入态凭据:notifications 是 copier 复制出的副本,置零安全,
// 不影响 singleton 内原始数据。
for _, n := range notifications {
n.URL = ""
n.RequestHeader = ""
n.RequestBody = ""
}
return notifications, nil
}
// Add notification
// @Summary Add notification
// @Security BearerAuth
// @Schemes
// @Description Add notification
// @Tags auth required
// @Accept json
// @param request body model.NotificationForm true "NotificationForm"
// @Produce json
// @Success 200 {object} model.CommonResponse[any]
// @Router /notification [post]
func createNotification(c *gin.Context) (uint64, error) {
var nf model.NotificationForm
if err := c.ShouldBindJSON(&nf); err != nil {
return 0, err
}
var n model.Notification
n.UserID = getUid(c)
n.Name = nf.Name
n.RequestMethod = nf.RequestMethod
n.RequestType = nf.RequestType
n.RequestHeader = nf.RequestHeader
n.RequestBody = nf.RequestBody
n.URL = nf.URL
verifyTLS := nf.VerifyTLS
n.VerifyTLS = &verifyTLS
formatMetricUnits := nf.FormatMetricUnits
n.FormatMetricUnits = &formatMetricUnits
ns := model.NotificationServerBundle{
Notification: &n,
Server: nil,
Loc: singleton.Loc,
}
// 未勾选跳过检查
if !nf.SkipCheck {
if err := ns.Send(singleton.Localizer.T("a test message")); err != nil {
return 0, err
}
}
if err := singleton.DB.Create(&n).Error; err != nil {
return 0, newGormError("%v", err)
}
singleton.NotificationShared.Update(&n)
return n.ID, nil
}
// Edit notification
// @Summary Edit notification
// @Security BearerAuth
// @Schemes
// @Description Edit notification
// @Tags auth required
// @Accept json
// @Param id path uint true "Notification ID"
// @Param body body model.NotificationForm true "NotificationForm"
// @Produce json
// @Success 200 {object} model.CommonResponse[any]
// @Router /notification/{id} [patch]
func updateNotification(c *gin.Context) (any, error) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return nil, err
}
var nf model.NotificationForm
if err := c.ShouldBindJSON(&nf); err != nil {
return nil, err
}
var n model.Notification
if err := singleton.DB.First(&n, id).Error; err != nil {
return nil, singleton.Localizer.ErrorT("notification id %d does not exist", id)
}
if !n.HasPermission(c) {
return nil, singleton.Localizer.ErrorT("permission denied")
}
n.Name = nf.Name
n.RequestMethod = nf.RequestMethod
n.RequestType = nf.RequestType
verifyTLS := nf.VerifyTLS
n.VerifyTLS = &verifyTLS
formatMetricUnits := nf.FormatMetricUnits
n.FormatMetricUnits = &formatMetricUnits
// 凭据在列表接口已脱敏,前端无法回填;空值视为"不修改",保留旧值避免误清空。
if nf.URL != "" {
n.URL = nf.URL
}
if nf.RequestHeader != "" {
n.RequestHeader = nf.RequestHeader
}
if nf.RequestBody != "" {
n.RequestBody = nf.RequestBody
}
ns := model.NotificationServerBundle{
Notification: &n,
Server: nil,
Loc: singleton.Loc,
}
// 未勾选跳过检查
if !nf.SkipCheck {
if err := ns.Send(singleton.Localizer.T("a test message")); err != nil {
return nil, err
}
}
if err := singleton.DB.Save(&n).Error; err != nil {
return nil, newGormError("%v", err)
}
singleton.NotificationShared.Update(&n)
return nil, nil
}
// Batch delete notifications
// @Summary Batch delete notifications
// @Security BearerAuth
// @Schemes
// @Description Batch delete notifications
// @Tags auth required
// @Accept json
// @param request body []uint64 true "id list"
// @Produce json
// @Success 200 {object} model.CommonResponse[any]
// @Router /batch-delete/notification [post]
func batchDeleteNotification(c *gin.Context) (any, error) {
var n []uint64
if err := c.ShouldBindJSON(&n); err != nil {
return nil, err
}
if !singleton.NotificationShared.CheckPermission(c, slices.Values(n)) {
return nil, singleton.Localizer.ErrorT("permission denied")
}
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Delete(&model.Notification{}, "id in (?)", n).Error; err != nil {
return err
}
if err := tx.Unscoped().Delete(&model.NotificationGroupNotification{}, "notification_id in (?)", n).Error; err != nil {
return err
}
return nil
})
if err != nil {
return nil, newGormError("%v", err)
}
singleton.NotificationShared.Delete(n)
return nil, nil
}