Files
agentic-coding-workflow/internal/infra/notify/prefs_handler.go
T
2026-06-22 08:55:57 +08:00

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
}
}