feat(backend): unified response helpers, refresh token, security hardening

- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
  envelope {detail, code?}, sanitized 500s (full error logged server-side,
  generic message to client - stops DB error leakage), pagination envelope
  {items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
  POST /auth/refresh with single-use rotation, POST /auth/logout revokes
  all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
  Instance.MarshalJSON guarantees encrypted passwords never leave the API
  (removes reliance on per-handler manual masking); production (DEBUG=false)
  with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
  gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
This commit is contained in:
Logan.Li committed 2026-10-26 17:21:00 +08:00
1 parent d5b8b8116c
commit 16f1f7d65f
26 files changed
+916 -963

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
// Package resp 统一 API 响应助手(渐进式契约统一):
// - 成功:直接返回业务数据;分页统一 {items, total, page, page_size} 信封
// - 失败:HTTP 状态码语义保持不变,错误体 {detail, code?}(兼容既有前端与测试)
// - 内部错误:服务端记录完整 error,客户端只收通用文案,杜绝 DB 错误原文泄漏
//
// 新代码禁止直接 c.JSON 错误响应;存量调用点在迭代中逐步迁移到本包。
package resp
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
// OK 200:直接返回业务数据
func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, data)
}
// Created 201:资源创建成功
func Created(c *gin.Context, data any) {
c.JSON(http.StatusCreated, data)
}
// Page 200:统一分页信封 {items, total, page, page_size}
func Page(c *gin.Context, items any, total int64, page, pageSize int) {
c.JSON(http.StatusOK, gin.H{
"items": items,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// Message 200:仅含 {message} 的轻量确认(删除/测试类端点)
func Message(c *gin.Context, msg string) {
c.JSON(http.StatusOK, gin.H{"message": msg})
}
// Fail 通用失败:{detail}
func Fail(c *gin.Context, status int, detail string) {
c.JSON(status, gin.H{"detail": detail})
}
// FailCode 失败并携带机器可读错误码:{detail, code}
func FailCode(c *gin.Context, status int, code, detail string) {
c.JSON(status, gin.H{"detail": detail, "code": code})
}
func BadRequest(c *gin.Context, detail string) { Fail(c, http.StatusBadRequest, detail) }
func Unauthorized(c *gin.Context, detail string) { Fail(c, http.StatusUnauthorized, detail) }
func Forbidden(c *gin.Context, detail string) { Fail(c, http.StatusForbidden, detail) }
func NotFound(c *gin.Context, detail string) { Fail(c, http.StatusNotFound, detail) }
// InternalErr 500:服务端 log 完整错误,客户端只收通用文案(防 DB/内部错误原文泄漏)
func InternalErr(c *gin.Context, err error) {
log.Printf("[resp] internal error on %s %s: %v", c.Request.Method, c.Request.URL.Path, err)
c.JSON(http.StatusInternalServerError, gin.H{"detail": "服务器内部错误,请稍后重试", "code": "internal"})
}
// InternalMsg 500:无底层 error、需要给用户可读文案时使用
func InternalMsg(c *gin.Context, userMsg string) {
c.JSON(http.StatusInternalServerError, gin.H{"detail": userMsg})
}
// TooManyRequests 429:携带 retry_after_minutes
func TooManyRequests(c *gin.Context, detail string, retryAfterMinutes int) {
c.JSON(http.StatusTooManyRequests, gin.H{"detail": detail, "retry_after_minutes": retryAfterMinutes})
}