fix(prod): 后端生产级 A 类硬伤全清(7 项:授权/崩溃点/全表扫/限流/鉴权/限额)
部署前生产级审计(可靠性/数据层/安全三路)后,清掉 7 处代码级硬伤: A1 后台定时器 goroutine 无 panic recover → 单个 DB panic 崩整个 gateway。加 safeGo/ safeCall,包住订阅/掉单补偿/微信推送/探针 goroutine,单轮 tick 再兜一层。 A2 提示词控制面(建/激活/停用,热广播全服务)只 RequireAuth → 任意登录用户改全局提示词。 三写端点+列表挂 RequireAdmin。 A3 HITL 审批端点无角色门 → viewer 可放行烧钱执行。加 RequireTenantRole(member)。 A4 审计/护栏列表 limit 无校验,limit=-1 让 gorm 取消 LIMIT 全表扫。加 clampLimit/ clampOffset,AdminTasks/AdminSpaces 补上界。 A5 限流 Redis 一挂就完全放行(fail-open)。加进程内固定窗口兜底(fail-safe) + 登录/注册 按 IP 专用严限流(10/min)。 A6 公开 by-id 端点(stream/exec/report导出/kb导入流)无鉴权无租户过滤。加 AuthFromHeaderOrQuery(从 ?token= 取 JWT) + task/report 按 owner 归属校验;桌面端 5 处 EventSource/下载 URL 经 tokenQuery 附 JWT。 A7 文件上传无大小上限(整文件进内存 OOM 面) → 50MB 闸(KB_MAX_UPLOAD_BYTES)+ LimitReader; http.Server 加 ReadHeaderTimeout/ReadTimeout/MaxHeaderBytes(不设 WriteTimeout 保 SSE)。 带单测:clampLimit/safeCall/procLimiter/AuthFromHeaderOrQuery/TaskOwner。 build+vet+全量 test 绿;desktop tsc 绿。B(迁移工具/实时探针/出网韧性/登录锁定/leader选举) 与 C(TLS/PG HA/K8s/备份自动化/可观测)分期后做,参照 production_readiness.md。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -59,15 +59,18 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
// 微信扫码登录(全公开:ticket 是唯一凭证;PC 建票与轮询都无鉴权头)
|
||||
api.POST("/wx/mp/ticket", h.WxMPTicket) // PC 建票 + 微信二维码
|
||||
api.GET("/wx/mp/poll", h.WxMPPoll) // PC 轮询登录态
|
||||
api.POST("/auth/register", h.Register) // 注册 + 签发 JWT
|
||||
api.POST("/auth/login", h.Login) // 登录 + 签发 JWT
|
||||
// 登录/注册爆破面大:在全局限流之上再叠一道更严的按 IP 专用限流(10/min)。
|
||||
api.POST("/auth/register", middleware.RateLimitN(cache, 10, "auth-reg"), h.Register) // 注册 + 签发 JWT
|
||||
api.POST("/auth/login", middleware.RateLimitN(cache, 10, "auth-login"), h.Login) // 登录 + 签发 JWT
|
||||
api.GET("/auth/me", h.Me) // 当前登录用户(无效令牌 → 401)
|
||||
api.GET("/health", h.Health) // 依赖健康聚合(顶栏五盏灯)
|
||||
api.GET("/tasks/:id/stream", h.StreamTask) // SSE 回流 Token Stream(task_id 寻址)
|
||||
api.GET("/tasks/:id/exec", h.StreamExec) // SSE 回流执行轨迹(task_id 寻址)
|
||||
api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSE(job_id 寻址)
|
||||
api.GET("/reports/:id/export", h.ExportReport) // 按需导出(report_id 寻址)
|
||||
api.GET("/reports/:id/download", h.ExportReport) // 兼容旧入口(默认 docx)
|
||||
// 这些 by-id 端点带不了 Bearer 头(EventSource/下载),改由 AuthFromHeaderOrQuery
|
||||
// 从 ?token= 取 JWT 强制鉴权;task/report 三个再按 owner 归属校验(handler 内)。
|
||||
api.GET("/tasks/:id/stream", middleware.AuthFromHeaderOrQuery(), h.StreamTask) // SSE 回流 Token Stream
|
||||
api.GET("/tasks/:id/exec", middleware.AuthFromHeaderOrQuery(), h.StreamExec) // SSE 回流执行轨迹
|
||||
api.GET("/kb/ingest/:id/stream", middleware.AuthFromHeaderOrQuery(), h.KbIngestStream) // 入库进度 SSE(登录即可,进度非敏感)
|
||||
api.GET("/reports/:id/export", middleware.AuthFromHeaderOrQuery(), h.ExportReport) // 按需导出
|
||||
api.GET("/reports/:id/download", middleware.AuthFromHeaderOrQuery(), h.ExportReport) // 兼容旧入口(默认 docx)
|
||||
api.POST("/billing/callback/:channel", h.PaymentCallback) // 支付回调(渠道服务器带不了 Bearer;渠道验签是唯一的门)
|
||||
|
||||
// —— 受保护:owner 作用域业务,必须携带有效 JWT ——
|
||||
@@ -75,7 +78,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
{
|
||||
p.POST("/tasks", middleware.RequireTenantRole(db, store.RoleMember), h.SubmitTask) // 提交任务(烧租户积分):viewer 只读拦下
|
||||
p.GET("/tasks/:id", h.TaskStatus) // 任务生命周期状态(UI 轮询 submitted/running/done/failed/timeout/waiting/rejected)
|
||||
p.POST("/tasks/:id/approve", middleware.Audit(db), h.ApproveTask) // HITL 人工审批决定(批准/拒绝,审计)
|
||||
p.POST("/tasks/:id/approve", middleware.RequireTenantRole(db, store.RoleMember), middleware.Audit(db), h.ApproveTask) // HITL 人工审批:≥member(放行会烧租户积分,viewer 拦下),审计
|
||||
p.GET("/tenants/current", h.TenantCurrent) // 当前租户上下文 + 角色 + 可花余额(多租户)
|
||||
p.GET("/me/tenants", h.MyTenantsList) // 我所属租户(供切换)
|
||||
p.POST("/me/tenants", h.CreateMyTenant) // 自助建组织(创建者即 owner,建完切入)
|
||||
@@ -104,11 +107,13 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
p.GET("/kb/doc", h.KbDoc) // 取单篇文档
|
||||
p.DELETE("/kb/doc", middleware.RequireSpaceRole(db, store.RoleMember), h.KbDeleteDoc) // 级联删文档:viewer 拦下
|
||||
|
||||
// Prompt 控制面(平台级配置:建版本 → 激活 → 控制面热下发各服务)
|
||||
p.GET("/prompts", h.PromptList) // 列出全部版本 + 可配键
|
||||
p.POST("/prompts/version", h.PromptCreateVersion) // 建新版本(不自动激活)
|
||||
p.POST("/prompts/activate", middleware.Audit(db), h.PromptActivate) // 激活某版本 → 广播热更新(审计)
|
||||
p.POST("/prompts/deactivate", middleware.Audit(db), h.PromptDeactivate) // 撤销激活 → 回退代码默认(热,审计)
|
||||
// Prompt 控制面(平台级配置:建版本 → 激活 → 控制面热下发各服务)。
|
||||
// 全平台配置、activate 热广播到所有服务、影响所有租户输出 —— 必须 RequireAdmin,
|
||||
// 否则任意登录用户可改全局提示词。读写同源都挂 admin。
|
||||
p.GET("/prompts", middleware.RequireAdmin(), h.PromptList) // 列出全部版本 + 可配键
|
||||
p.POST("/prompts/version", middleware.RequireAdmin(), middleware.Audit(db), h.PromptCreateVersion) // 建新版本(不自动激活)
|
||||
p.POST("/prompts/activate", middleware.RequireAdmin(), middleware.Audit(db), h.PromptActivate) // 激活某版本 → 广播热更新(审计)
|
||||
p.POST("/prompts/deactivate", middleware.RequireAdmin(), middleware.Audit(db), h.PromptDeactivate) // 撤销激活 → 回退代码默认(热,审计)
|
||||
p.GET("/kb/links", h.KbLinks) // 某库双链
|
||||
p.POST("/kb/note", middleware.RequireSpaceRole(db, store.RoleMember), h.KbSaveNote) // 新建/编辑笔记:viewer 拦下
|
||||
p.GET("/kb/graph", h.KbGraph) // 知识图谱三元组
|
||||
|
||||
Reference in New Issue
Block a user