Files
backend/internal/pkg/response/response.go
lafay eb931bf1c6
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 46s
feat(auth): implement session-based token management with revocation support
Add Session model, SessionService, and SessionRepository to track user login
sessions and enable token revocation on auth-critical events.

- Introduce explicit TokenType (access/refresh) in JWT claims to prevent
  refresh token misuse via access token endpoints
- Add SessionID field to JWT claims, enabling stateless JWT validation
  against revoked sessions
- Replace legacy Auth middleware with RequireAuth/OptionalAuth pipeline
  that validates token type, account status, and session validity
- Implement session revocation on password change, reset, user ban/inactive,
  and explicit logout
- Add Principal cache with active invalidation for banned/role-changed users
- Fix IDOR vulnerability: GetMessagesByCursor now validates currentUserID
  is conversation participant via GetParticipantStrict
- Add group member visibility checks: announcements, group info, member list
  now require group membership
- Simplify Casbin policy: remove g grouping, use r.sub == p.sub matcher
  with globMatch; user_roles table is single source of truth for roles
- Add migration logic to clean legacy casbin g rules and migrate old p rules
  from path-style to admin/<domain> resource naming
2026-07-05 18:28:08 +08:00

175 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package response
import (
"net/http"
"github.com/gin-gonic/gin"
apperrors "with_you/internal/errors"
)
// Response 统一响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
// Success 成功响应
func Success(c *gin.Context, data any) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: data,
})
}
// SuccessWithMessage 成功响应带消息
func SuccessWithMessage(c *gin.Context, message string, data any) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: message,
Data: data,
})
}
// Error 错误响应
func Error(c *gin.Context, code int, message string) {
c.JSON(http.StatusBadRequest, Response{
Code: code,
Message: message,
})
}
// ErrorWithStatusCode 带状态码的错误响应
func ErrorWithStatusCode(c *gin.Context, statusCode int, code int, message string) {
c.JSON(statusCode, Response{
Code: code,
Message: message,
})
}
// BadRequest 参数错误
func BadRequest(c *gin.Context, message string) {
ErrorWithStatusCode(c, http.StatusBadRequest, 400, message)
}
// Unauthorized 未授权
func Unauthorized(c *gin.Context, message string) {
if message == "" {
message = "unauthorized"
}
ErrorWithStatusCode(c, http.StatusUnauthorized, 401, message)
}
// Forbidden 禁止访问
func Forbidden(c *gin.Context, message string) {
if message == "" {
message = "forbidden"
}
ErrorWithStatusCode(c, http.StatusForbidden, 403, message)
}
// NotFound 资源不存在
func NotFound(c *gin.Context, message string) {
if message == "" {
message = "resource not found"
}
ErrorWithStatusCode(c, http.StatusNotFound, 404, message)
}
// InternalServerError 服务器内部错误
func InternalServerError(c *gin.Context, message string) {
if message == "" {
message = "internal server error"
}
ErrorWithStatusCode(c, http.StatusInternalServerError, 500, message)
}
// PaginatedResponse 分页响应
type PaginatedResponse struct {
List any `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// Paginated 分页成功响应
func Paginated(c *gin.Context, list any, total int64, page, pageSize int) {
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
Success(c, PaginatedResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
})
}
// HandleError 统一处理错误(带默认消息)
// 如果 err 是 *apperrors.AppError返回对应的业务错误码和消息
// 如果 err 是其他错误,返回 InternalServerError 并使用 defaultMessage
func HandleError(c *gin.Context, err error, defaultMessage string) {
if err == nil {
return
}
if se, ok := apperrors.As(err); ok {
statusCode := statusCodeFromAppErrorCode(se.Code)
ErrorWithStringCode(c, statusCode, se.Code, se.Message)
return
}
InternalServerError(c, defaultMessage)
}
// statusCodeFromAppErrorCode 根据AppError的错误码字符串获取 HTTP 状态码
func statusCodeFromAppErrorCode(code string) int {
switch code {
case "NOT_FOUND", "USER_NOT_FOUND", "POST_NOT_FOUND", "GROUP_NOT_FOUND",
"GROUP_REQUEST_NOT_FOUND", "SCHEDULE_COURSE_NOT_FOUND",
"VERIFICATION_NOT_FOUND", "CALL_NOT_FOUND":
return http.StatusNotFound
case "UNAUTHORIZED", "INVALID_TOKEN", "TOKEN_EXPIRED", "SESSION_REVOKED":
return http.StatusUnauthorized
case "INVALID_CREDENTIALS":
return http.StatusBadRequest
case "FORBIDDEN", "NOT_GROUP_MEMBER", "NOT_GROUP_ADMIN", "NOT_GROUP_OWNER",
"USER_BANNED", "USER_BLOCKED", "MUTED", "MUTE_ALL_ENABLED",
"SCHEDULE_FORBIDDEN", "UNAUTHORIZED_NOTIFICATION",
"ACCOUNT_INACTIVE", "WRONG_TOKEN_TYPE", "VERIFICATION_REQUIRED",
"SETUP_ALREADY_COMPLETED":
return http.StatusForbidden
case "BAD_REQUEST", "INVALID_USERNAME", "INVALID_EMAIL", "INVALID_PHONE",
"WEAK_PASSWORD", "USERNAME_EXISTS", "EMAIL_EXISTS", "PHONE_EXISTS",
"USER_ALREADY_EXISTS", "INVALID_OPERATION", "VERIFICATION_CODE_INVALID",
"VERIFICATION_CODE_EXPIRED", "EMAIL_ALREADY_VERIFIED", "EMAIL_NOT_BOUND",
"GROUP_FULL", "ALREADY_GROUP_MEMBER", "CANNOT_REMOVE_OWNER", "CANNOT_MUTE_OWNER",
"CANNOT_JOIN", "JOIN_REQUEST_PENDING", "GROUP_REQUEST_HANDLED",
"NOT_REQUEST_TARGET", "NO_ELIGIBLE_INVITEE", "NOT_MUTUAL_FOLLOW",
"INVITE_NOT_ACCEPTED", "STICKER_ALREADY_EXISTS", "INVALID_STICKER_URL",
"INVALID_SCHEDULE_PAYLOAD", "SCHEDULE_COLOR_DUPLICATED",
"INVALID_SETUP_SECRET", "SETUP_SECRET_NOT_CONFIGURED":
return http.StatusBadRequest
case "VERIFICATION_CODE_TOO_FREQUENT":
return http.StatusTooManyRequests
case "EMAIL_SERVICE_UNAVAILABLE", "VERIFICATION_CODE_UNAVAILABLE", "INTERNAL_ERROR":
return http.StatusInternalServerError
default:
return http.StatusBadRequest
}
}
// ErrorWithStringCode 带字符串错误码的错误响应
func ErrorWithStringCode(c *gin.Context, statusCode int, code string, message string) {
c.JSON(statusCode, gin.H{
"code": statusCode,
"message": message,
"error_code": code,
})
}