| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- // Package httpx 提供跨领域共用的 HTTP 辅助。
- //
- // 依据 docs/architecture.md 5.4:统一错误响应构造函数放在这里,
- // 所有 handler 调用同一套;错误细节不返回给客户端。
- package httpx
- import (
- "errors"
- "log/slog"
- "net/http"
- "github.com/gin-gonic/gin"
- "github.com/kairo/haven/internal/auth"
- )
- // ErrorBody 是 api.md 1.4 定义的错误响应。
- // fields 仅在 422 validation_failed 时出现。
- type ErrorBody struct {
- Error string `json:"error"`
- Message string `json:"message"`
- Fields map[string]string `json:"fields,omitempty"`
- }
- // 错误码常量,取值见 docs/api.md 1.4。
- const (
- CodeBadRequest = "bad_request"
- CodeUnauthorized = "unauthorized"
- CodeForbidden = "forbidden"
- CodeNotFound = "not_found"
- CodeConflict = "conflict"
- CodeValidationFailed = "validation_failed"
- CodeInternal = "internal_error"
- )
- // FailInternal 返回 500,细节只进日志(api.md 1.4、architecture.md 5.4)。
- func FailInternal(c *gin.Context, err error) {
- if err != nil {
- slog.Error("请求处理失败", "method", c.Request.Method, "path", c.Request.URL.Path, "err", err)
- }
- c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorBody{
- Error: CodeInternal,
- Message: "服务端错误",
- })
- }
- // FailUnauthorized 返回 401。
- func FailUnauthorized(c *gin.Context) {
- FailUnauthorizedWith(c, "未登录或凭证无效")
- }
- // FailUnauthorizedWith 返回 401,并带自定义 message(如“动态码错误”)。
- func FailUnauthorizedWith(c *gin.Context, message string) {
- c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorBody{
- Error: CodeUnauthorized,
- Message: message,
- })
- }
- // FailBadRequest 返回 400(JSON 解析失败等),解析细节只进日志。
- func FailBadRequest(c *gin.Context, err error) {
- if err != nil {
- slog.Debug("请求体解析失败", "method", c.Request.Method, "path", c.Request.URL.Path, "err", err)
- }
- FailBadRequestWith(c, "请求体格式错误")
- }
- // FailBadRequestWith 返回 400,并带自定义 message。
- func FailBadRequestWith(c *gin.Context, message string) {
- c.AbortWithStatusJSON(http.StatusBadRequest, ErrorBody{
- Error: CodeBadRequest,
- Message: message,
- })
- }
- // FailNotFound 返回 404。
- func FailNotFound(c *gin.Context) {
- c.AbortWithStatusJSON(http.StatusNotFound, ErrorBody{
- Error: CodeNotFound,
- Message: "资源不存在",
- })
- }
- // FailConflict 返回 409。
- func FailConflict(c *gin.Context, message string) {
- c.AbortWithStatusJSON(http.StatusConflict, ErrorBody{
- Error: CodeConflict,
- Message: message,
- })
- }
- // FailUnprocessable 返回 422,但不带 fields(语义化校验失败)。
- func FailUnprocessable(c *gin.Context, message string) {
- c.AbortWithStatusJSON(http.StatusUnprocessableEntity, ErrorBody{
- Error: CodeValidationFailed,
- Message: message,
- })
- }
- // FailForbidden 返回 403。
- func FailForbidden(c *gin.Context) {
- c.AbortWithStatusJSON(http.StatusForbidden, ErrorBody{
- Error: CodeForbidden,
- Message: "权限不足",
- })
- }
- // FailValidation 返回 422,并带 fields。
- func FailValidation(c *gin.Context, fields map[string]string) {
- c.AbortWithStatusJSON(http.StatusUnprocessableEntity, ErrorBody{
- Error: CodeValidationFailed,
- Message: "字段校验失败",
- Fields: fields,
- })
- }
- // Fail 按领域错误映射成 api.md 1.4 的响应。
- //
- // 只映射认证包已定义的业务错误;未识别的错误一律 500。
- func Fail(c *gin.Context, err error) {
- switch {
- case errors.Is(err, auth.ErrInvalidCredentials):
- c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorBody{
- Error: CodeUnauthorized,
- Message: auth.ErrInvalidCredentials.Error(),
- })
- case errors.Is(err, auth.ErrPendingExpired):
- c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorBody{
- Error: CodeUnauthorized,
- Message: auth.ErrPendingExpired.Error(),
- })
- case errors.Is(err, auth.ErrBadTotp):
- c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorBody{
- Error: CodeUnauthorized,
- Message: auth.ErrBadTotp.Error(),
- })
- case errors.Is(err, auth.ErrTotpAlreadyEnabled):
- c.AbortWithStatusJSON(http.StatusConflict, ErrorBody{
- Error: CodeConflict,
- Message: auth.ErrTotpAlreadyEnabled.Error(),
- })
- case errors.Is(err, auth.ErrTotpNotEnabled):
- FailValidation(c, map[string]string{"code": "尚未启用 TOTP"})
- case errors.Is(err, auth.ErrNotFound):
- c.AbortWithStatusJSON(http.StatusNotFound, ErrorBody{
- Error: CodeNotFound,
- Message: "资源不存在",
- })
- default:
- FailInternal(c, err)
- }
- }
- // InstallAuthErrorHandlers 把统一错误响应注入 auth 包,
- // 使鉴权中间件不必依赖 httpx(避免包循环)。
- func InstallAuthErrorHandlers() {
- auth.SetErrorHandlers(
- func(c *gin.Context, _ error) { FailUnauthorized(c) },
- func(c *gin.Context, _ error) { FailForbidden(c) },
- FailInternal,
- )
- }
|