You've already forked agentic-coding-workflow
134 lines
4.5 KiB
Go
134 lines
4.5 KiB
Go
// Package notify 内的 prefs_handler.go 实现 per-user 通知偏好的 HTTP 接口:
|
|
//
|
|
// GET /api/v1/notifications/prefs → 当前用户的偏好(im_webhook_url 不回显明文,
|
|
// 仅返回 im_webhook_url_set 布尔标记是否已配置)。
|
|
// PUT /api/v1/notifications/prefs → 整体更新偏好。im_webhook_url 提供非空字符串
|
|
// 则加密落库;提供空字符串则清除;字段省略则保留原值。
|
|
//
|
|
// im_webhook_url 经 Encryptor 加密为 im_webhook_url_enc BYTEA。偏好只在 Handler
|
|
// 注入了 prefsRepo + enc 后才挂载(WithPrefs),未配置 crypto 时该路由不暴露。
|
|
package notify
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
|
|
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
|
|
)
|
|
|
|
// prefsDTO 是偏好的对外 JSON 形态。im_webhook_url 绝不回显明文:读取时只暴露
|
|
// im_webhook_url_set 标记是否已配置,写入时通过单独的可空字段提交。
|
|
type prefsDTO struct {
|
|
EmailEnabled bool `json:"email_enabled"`
|
|
IMEnabled bool `json:"im_enabled"`
|
|
IMWebhookURLSet bool `json:"im_webhook_url_set"`
|
|
MinSeverity string `json:"min_severity"`
|
|
}
|
|
|
|
// prefsUpdateReq 是 PUT 请求体。IMWebhookURL 用指针区分三态:
|
|
// - nil(字段省略) → 保留原密文不变;
|
|
// - 指向空字符串 "" → 清除已配置的 webhook;
|
|
// - 指向非空字符串 → 加密为新密文。
|
|
type prefsUpdateReq struct {
|
|
EmailEnabled *bool `json:"email_enabled"`
|
|
IMEnabled *bool `json:"im_enabled"`
|
|
IMWebhookURL *string `json:"im_webhook_url"`
|
|
MinSeverity *string `json:"min_severity"`
|
|
}
|
|
|
|
// getPrefs 处理 GET /api/v1/notifications/prefs。
|
|
func (h *Handler) getPrefs(w http.ResponseWriter, r *http.Request) {
|
|
uid, ok := middleware.UserIDFromContext(r.Context())
|
|
if !ok {
|
|
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()),
|
|
errs.New(errs.CodeUnauthorized, "未认证"))
|
|
return
|
|
}
|
|
p, err := h.prefs.Get(r.Context(), uid)
|
|
if err != nil {
|
|
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err)
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, prefsDTO{
|
|
EmailEnabled: p.EmailEnabled,
|
|
IMEnabled: p.IMEnabled,
|
|
IMWebhookURLSet: len(p.ImWebhookURLEnc) > 0,
|
|
MinSeverity: string(p.MinSeverity),
|
|
})
|
|
}
|
|
|
|
// putPrefs 处理 PUT /api/v1/notifications/prefs。
|
|
func (h *Handler) putPrefs(w http.ResponseWriter, r *http.Request) {
|
|
uid, ok := middleware.UserIDFromContext(r.Context())
|
|
if !ok {
|
|
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()),
|
|
errs.New(errs.CodeUnauthorized, "未认证"))
|
|
return
|
|
}
|
|
var req prefsUpdateReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()),
|
|
errs.New(errs.CodeInvalidInput, "请求体格式错误"))
|
|
return
|
|
}
|
|
|
|
cur, err := h.prefs.Get(r.Context(), uid)
|
|
if err != nil {
|
|
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err)
|
|
return
|
|
}
|
|
if req.EmailEnabled != nil {
|
|
cur.EmailEnabled = *req.EmailEnabled
|
|
}
|
|
if req.IMEnabled != nil {
|
|
cur.IMEnabled = *req.IMEnabled
|
|
}
|
|
if req.MinSeverity != nil {
|
|
sev := Severity(strings.TrimSpace(*req.MinSeverity))
|
|
if !validSeverity(sev) {
|
|
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()),
|
|
errs.New(errs.CodeInvalidInput, "min_severity 取值非法(info|warning|error)"))
|
|
return
|
|
}
|
|
cur.MinSeverity = sev
|
|
}
|
|
if req.IMWebhookURL != nil {
|
|
url := strings.TrimSpace(*req.IMWebhookURL)
|
|
if url == "" {
|
|
cur.ImWebhookURLEnc = nil
|
|
} else {
|
|
ct, err := h.enc.Encrypt([]byte(url))
|
|
if err != nil {
|
|
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()),
|
|
errs.Wrap(err, errs.CodeInternal, "加密 im_webhook_url"))
|
|
return
|
|
}
|
|
cur.ImWebhookURLEnc = ct
|
|
}
|
|
}
|
|
|
|
if err := h.prefs.Upsert(r.Context(), cur); err != nil {
|
|
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err)
|
|
return
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, prefsDTO{
|
|
EmailEnabled: cur.EmailEnabled,
|
|
IMEnabled: cur.IMEnabled,
|
|
IMWebhookURLSet: len(cur.ImWebhookURLEnc) > 0,
|
|
MinSeverity: string(cur.MinSeverity),
|
|
})
|
|
}
|
|
|
|
// validSeverity 报告 s 是否为合法的 min_severity 取值。
|
|
func validSeverity(s Severity) bool {
|
|
switch s {
|
|
case SeverityInfo, SeverityWarning, SeverityError:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|