package api import ( "errors" "net/http" "github.com/gin-gonic/gin" "oci-portal/internal/service" ) // settingsHandler 处理系统设置(Telegram 通知)相关请求。 type settingsHandler struct { svc *service.SettingService notifier *service.Notifier } // getTelegram 返回脱敏后的 Telegram 配置,绝不回 token 明文。 // // @Summary 返回脱敏后的 Telegram 配置,绝不回 token 明文 // @Tags 设置 // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/telegram [get] func (h *settingsHandler) getTelegram(c *gin.Context) { view, err := h.svc.TelegramView(c.Request.Context()) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, view) } // updateTelegramRequest 是保存 Telegram 配置的请求体; // botToken 缺省表示沿用已存 token,空串表示清除。 type updateTelegramRequest struct { Enabled bool `json:"enabled"` BotToken *string `json:"botToken"` ChatID string `json:"chatId"` } // updateTelegram 保存配置并返回最新脱敏视图。 // // @Summary 保存配置并返回最新脱敏视图 // @Tags 设置 // @Param body body updateTelegramRequest true "请求体" // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/telegram [put] func (h *settingsHandler) updateTelegram(c *gin.Context) { var req updateTelegramRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err := h.svc.UpdateTelegram(c.Request.Context(), service.UpdateTelegramInput{ Enabled: req.Enabled, BotToken: req.BotToken, ChatID: req.ChatID, }) if err != nil { respondError(c, err) return } h.getTelegram(c) } // testTelegram 用当前已保存配置同步发送一条测试消息。 // // @Summary 用当前已保存配置同步发送一条测试消息 // @Tags 设置 // @Success 204 "无内容" // @Security BearerAuth // @Router /api/v1/settings/telegram/test [post] func (h *settingsHandler) testTelegram(c *gin.Context) { if err := h.notifier.Test(c.Request.Context()); err != nil { respondError(c, err) return } c.Status(http.StatusNoContent) } // listNotifyChannels 返回全部通知渠道的脱敏视图(webhook/ntfy/bark/smtp,不含 telegram)。 // // @Summary 返回全部通知渠道的脱敏视图 // @Tags 设置 // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/notify-channels [get] func (h *settingsHandler) listNotifyChannels(c *gin.Context) { items, err := h.svc.NotifyChannels(c.Request.Context()) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, gin.H{"items": items}) } // updateNotifyChannelRequest 是保存单渠道配置的请求体; // secret 承载渠道密文字段(ntfy token / bark device key / smtp 密码), // 缺省沿用已存值,空串清除。 type updateNotifyChannelRequest struct { Enabled bool `json:"enabled"` URL string `json:"url"` BodyTemplate string `json:"bodyTemplate"` Server string `json:"server"` Topic string `json:"topic"` Host string `json:"host"` Port int `json:"port"` Username string `json:"username"` From string `json:"from"` To string `json:"to"` Secret *string `json:"secret"` } // updateNotifyChannel 保存单渠道配置并返回全渠道最新视图;未知类型或校验失败返回 400。 // // @Summary 保存单渠道配置并返回全渠道最新视图 // @Tags 设置 // @Param type path string true "渠道类型(webhook/ntfy/bark/smtp)" // @Param body body updateNotifyChannelRequest true "请求体" // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/notify-channels/{type} [put] func (h *settingsHandler) updateNotifyChannel(c *gin.Context) { var req updateNotifyChannelRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err := h.svc.UpdateNotifyChannel(c.Request.Context(), c.Param("type"), service.UpdateNotifyChannelInput{ Enabled: req.Enabled, URL: req.URL, BodyTemplate: req.BodyTemplate, Server: req.Server, Topic: req.Topic, Host: req.Host, Port: req.Port, Username: req.Username, From: req.From, To: req.To, Secret: req.Secret, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } h.listNotifyChannels(c) } // testNotifyChannel 用已保存配置向指定渠道同步发送测试消息。 // // @Summary 向指定渠道发送测试消息 // @Tags 设置 // @Param type path string true "渠道类型(webhook/ntfy/bark/smtp)" // @Success 204 "无内容" // @Security BearerAuth // @Router /api/v1/settings/notify-channels/{type}/test [post] func (h *settingsHandler) testNotifyChannel(c *gin.Context) { if err := h.notifier.SendChannelTest(c.Request.Context(), c.Param("type")); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.Status(http.StatusNoContent) } // getNotifyEvents 返回全部通知事件开关;键缺省(存量部署)视为开启。 // // @Summary 返回全部通知事件开关 // @Tags 设置 // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/notify-events [get] func (h *settingsHandler) getNotifyEvents(c *gin.Context) { view, err := h.svc.NotifyEvents(c.Request.Context()) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, view) } // updateNotifyEvents 全量保存五个事件开关并返回最新值; // 请求体为全量语义,缺字段按 JSON 零值 false 处理。 // // @Summary 全量保存五个事件开关并返回最新值 // @Tags 设置 // @Param body body service.NotifyEventsView true "请求体" // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/notify-events [put] func (h *settingsHandler) updateNotifyEvents(c *gin.Context) { var req service.NotifyEventsView if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.svc.UpdateNotifyEvents(c.Request.Context(), req); err != nil { respondError(c, err) return } h.getNotifyEvents(c) } // listNotifyTemplates 返回全部通知模板(默认模板 + 自定义现值 + 可用变量)。 // // @Summary 返回全部通知模板 // @Tags 设置 // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/notify-templates [get] func (h *settingsHandler) listNotifyTemplates(c *gin.Context) { items, err := h.svc.NotifyTemplates(c.Request.Context()) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, gin.H{"items": items}) } // updateNotifyTemplate 保存单个通知模板;空模板清除自定义(恢复默认)。 // // @Summary 保存单个通知模板 // @Tags 设置 // @Param kind path string true "kind" // @Param body body object true "请求体(见接口说明)" // @Success 204 "无内容" // @Security BearerAuth // @Router /api/v1/settings/notify-templates/{kind} [put] func (h *settingsHandler) updateNotifyTemplate(c *gin.Context) { var req struct { Template string `json:"template"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.svc.UpdateNotifyTemplate(c.Request.Context(), c.Param("kind"), req.Template); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.Status(http.StatusNoContent) } // testNotifyTemplate 用示例变量渲染模板并同步发送测试消息; // template 非空时按编辑中内容渲染(不落库)。 // // @Summary 用示例变量渲染模板并同步发送测试消息 // @Tags 设置 // @Param kind path string true "kind" // @Param body body object true "请求体(见接口说明)" // @Success 204 "无内容" // @Security BearerAuth // @Router /api/v1/settings/notify-templates/{kind}/test [post] func (h *settingsHandler) testNotifyTemplate(c *gin.Context) { var req struct { Template string `json:"template"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.notifier.SendTemplateTest(c.Request.Context(), c.Param("kind"), req.Template); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.Status(http.StatusNoContent) } // getTaskSettings 返回任务行为设置(抢机熔断阈值),阈值未保存过时为默认值。 // // @Summary 返回任务行为设置 // @Tags 设置 // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/task [get] func (h *settingsHandler) getTaskSettings(c *gin.Context) { view, err := h.svc.TaskSettings(c.Request.Context()) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, view) } // updateTaskSettings 保存任务行为设置并返回最新值;阈值越界返回 400。 // // @Summary 保存任务行为设置并返回最新值 // @Tags 设置 // @Param body body service.TaskSettingsView true "请求体" // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/task [put] func (h *settingsHandler) updateTaskSettings(c *gin.Context) { var req service.TaskSettingsView if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.svc.UpdateTaskSettings(c.Request.Context(), req); err != nil { if errors.Is(err, service.ErrInvalidAuthFailLimit) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } respondError(c, err) return } h.getTaskSettings(c) } // getSecurity 返回安全设置(WAF 参数 / 真实IP请求头 / 面板地址),未保存过时为默认值。 // // @Summary 返回安全设置 // @Tags 设置 // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/security [get] func (h *settingsHandler) getSecurity(c *gin.Context) { view, err := h.svc.Security(c.Request.Context()) if err != nil { respondError(c, err) return } c.JSON(http.StatusOK, view) } // updateSecurity 保存安全设置并返回最新值;越界或非法返回 400,保存后立即生效。 // // @Summary 保存安全设置并返回最新值 // @Tags 设置 // @Param body body service.SecuritySettings true "请求体" // @Success 200 {object} map[string]any // @Security BearerAuth // @Router /api/v1/settings/security [put] func (h *settingsHandler) updateSecurity(c *gin.Context) { var req service.SecuritySettings if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.svc.UpdateSecurity(c.Request.Context(), req); err != nil { if errors.Is(err, service.ErrInvalidSecurity) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } respondError(c, err) return } h.getSecurity(c) }