Files
backend/internal/errors/app_errors.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

174 lines
11 KiB
Go
Raw Permalink 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 errors
import (
"errors"
"fmt"
)
// AppError 统一的应用错误类型
type AppError struct {
Code string
Message string
Cause error
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// 预定义错误
var (
// 通用错误
ErrNotFound = &AppError{Code: "NOT_FOUND", Message: "资源不存在"}
ErrUnauthorized = &AppError{Code: "UNAUTHORIZED", Message: "未授权"}
ErrForbidden = &AppError{Code: "FORBIDDEN", Message: "禁止访问"}
ErrBadRequest = &AppError{Code: "BAD_REQUEST", Message: "请求参数错误"}
ErrInternal = &AppError{Code: "INTERNAL_ERROR", Message: "内部服务器错误"}
// 用户相关错误
ErrUserNotFound = &AppError{Code: "USER_NOT_FOUND", Message: "用户不存在"}
ErrUserAlreadyExists = &AppError{Code: "USER_ALREADY_EXISTS", Message: "用户已存在"}
ErrInvalidCredentials = &AppError{Code: "INVALID_CREDENTIALS", Message: "用户名或密码错误"}
ErrInvalidUsername = &AppError{Code: "INVALID_USERNAME", Message: "无效的用户名"}
ErrInvalidEmail = &AppError{Code: "INVALID_EMAIL", Message: "无效的邮箱"}
ErrInvalidPhone = &AppError{Code: "INVALID_PHONE", Message: "无效的手机号"}
ErrWeakPassword = &AppError{Code: "WEAK_PASSWORD", Message: "密码强度不足"}
ErrUsernameExists = &AppError{Code: "USERNAME_EXISTS", Message: "用户名已存在"}
ErrEmailExists = &AppError{Code: "EMAIL_EXISTS", Message: "邮箱已存在"}
ErrPhoneExists = &AppError{Code: "PHONE_EXISTS", Message: "手机号已存在"}
ErrUserBanned = &AppError{Code: "USER_BANNED", Message: "用户已被封禁"}
ErrUserBlocked = &AppError{Code: "USER_BLOCKED", Message: "存在屏蔽关系"}
ErrInvalidOperation = &AppError{Code: "INVALID_OPERATION", Message: "无效操作"}
ErrEmailServiceUnavailable = &AppError{Code: "EMAIL_SERVICE_UNAVAILABLE", Message: "邮件服务不可用"}
ErrVerificationCodeTooFrequent = &AppError{Code: "VERIFICATION_CODE_TOO_FREQUENT", Message: "验证码发送过于频繁"}
ErrVerificationCodeInvalid = &AppError{Code: "VERIFICATION_CODE_INVALID", Message: "验证码无效"}
ErrVerificationCodeExpired = &AppError{Code: "VERIFICATION_CODE_EXPIRED", Message: "验证码已过期"}
ErrVerificationCodeUnavailable = &AppError{Code: "VERIFICATION_CODE_UNAVAILABLE", Message: "验证码存储不可用"}
ErrEmailAlreadyVerified = &AppError{Code: "EMAIL_ALREADY_VERIFIED", Message: "邮箱已验证"}
ErrEmailNotBound = &AppError{Code: "EMAIL_NOT_BOUND", Message: "邮箱未绑定"}
// 帖子相关错误
ErrPostNotFound = &AppError{Code: "POST_NOT_FOUND", Message: "帖子不存在"}
ErrPostDeleted = &AppError{Code: "POST_DELETED", Message: "帖子已删除"}
// 群组相关错误
ErrGroupNotFound = &AppError{Code: "GROUP_NOT_FOUND", Message: "群组不存在"}
ErrNotGroupMember = &AppError{Code: "NOT_GROUP_MEMBER", Message: "不是群组成员"}
ErrAlreadyGroupMember = &AppError{Code: "ALREADY_GROUP_MEMBER", Message: "已是群组成员"}
ErrNotGroupAdmin = &AppError{Code: "NOT_GROUP_ADMIN", Message: "不是群管理员"}
ErrNotGroupOwner = &AppError{Code: "NOT_GROUP_OWNER", Message: "不是群主"}
ErrGroupFull = &AppError{Code: "GROUP_FULL", Message: "群已满"}
ErrCannotRemoveOwner = &AppError{Code: "CANNOT_REMOVE_OWNER", Message: "不能移除群主"}
ErrCannotMuteOwner = &AppError{Code: "CANNOT_MUTE_OWNER", Message: "不能禁言群主"}
ErrMuted = &AppError{Code: "MUTED", Message: "你已被禁言"}
ErrMuteAllEnabled = &AppError{Code: "MUTE_ALL_ENABLED", Message: "全员禁言中"}
ErrCannotJoin = &AppError{Code: "CANNOT_JOIN", Message: "该群不允许加入"}
ErrJoinRequestPending = &AppError{Code: "JOIN_REQUEST_PENDING", Message: "加群申请已提交"}
ErrGroupRequestNotFound = &AppError{Code: "GROUP_REQUEST_NOT_FOUND", Message: "加群请求不存在"}
ErrGroupRequestHandled = &AppError{Code: "GROUP_REQUEST_HANDLED", Message: "该加群请求已处理"}
ErrNotRequestTarget = &AppError{Code: "NOT_REQUEST_TARGET", Message: "不是邀请目标用户"}
ErrNoEligibleInvitee = &AppError{Code: "NO_ELIGIBLE_INVITEE", Message: "没有可邀请的用户"}
ErrNotMutualFollow = &AppError{Code: "NOT_MUTUAL_FOLLOW", Message: "仅支持邀请互相关注用户"}
ErrInviteNotAccepted = &AppError{Code: "INVITE_NOT_ACCEPTED", Message: "被邀请人尚未同意邀请,无法审批"}
// 表情包相关错误
ErrStickerAlreadyExists = &AppError{Code: "STICKER_ALREADY_EXISTS", Message: "表情包已存在"}
ErrInvalidStickerURL = &AppError{Code: "INVALID_STICKER_URL", Message: "无效的表情包URL"}
// 日程相关错误
ErrInvalidSchedulePayload = &AppError{Code: "INVALID_SCHEDULE_PAYLOAD", Message: "无效的日程数据"}
ErrScheduleCourseNotFound = &AppError{Code: "SCHEDULE_COURSE_NOT_FOUND", Message: "日程课程不存在"}
ErrScheduleForbidden = &AppError{Code: "SCHEDULE_FORBIDDEN", Message: "禁止的日程操作"}
ErrScheduleColorDuplicated = &AppError{Code: "SCHEDULE_COLOR_DUPLICATED", Message: "课程颜色已被使用"}
// 通知相关错误
ErrUnauthorizedNotification = &AppError{Code: "UNAUTHORIZED_NOTIFICATION", Message: "无权删除此通知"}
// Casbin 相关错误
ErrPermissionDenied = &AppError{Code: "PERMISSION_DENIED", Message: "权限不足"}
ErrRoleNotFound = &AppError{Code: "ROLE_NOT_FOUND", Message: "角色不存在"}
ErrRoleAlreadyAssigned = &AppError{Code: "ROLE_ALREADY_ASSIGNED", Message: "用户已拥有该角色"}
ErrRoleNotAssigned = &AppError{Code: "ROLE_NOT_ASSIGNED", Message: "用户未拥有该角色"}
ErrCannotModifyOwnRole = &AppError{Code: "CANNOT_MODIFY_OWN_ROLE", Message: "不能修改自己的角色"}
ErrCannotModifySuperAdmin = &AppError{Code: "CANNOT_MODIFY_SUPER_ADMIN", Message: "不能修改超级管理员角色"}
ErrCasbinInternal = &AppError{Code: "CASBIN_INTERNAL_ERROR", Message: "权限系统内部错误"}
// 通话相关错误
ErrCallNotFound = &AppError{Code: "CALL_NOT_FOUND", Message: "通话不存在"}
ErrCallNotActive = &AppError{Code: "CALL_NOT_ACTIVE", Message: "通话未激活"}
ErrCallInProgress = &AppError{Code: "CALL_IN_PROGRESS", Message: "用户之间已有通话"}
ErrNotCallParticipant = &AppError{Code: "NOT_CALL_PARTICIPANT", Message: "不是通话参与者"}
ErrInvalidCallState = &AppError{Code: "INVALID_CALL_STATE", Message: "通话状态无效"}
ErrCalleeOffline = &AppError{Code: "CALLEE_OFFLINE", Message: "对方不在线"}
ErrCallAlreadyAnswered = &AppError{Code: "CALL_ALREADY_ANSWERED", Message: "通话已在其他设备应答"}
ErrGroupCallNotSupported = &AppError{Code: "GROUP_CALL_NOT_SUPPORTED", Message: "群组通话不支持"}
ErrMaxParticipantsReached = &AppError{Code: "MAX_PARTICIPANTS_REACHED", Message: "通话人数已达上限"}
// 消息相关错误
ErrConversationNotFound = &AppError{Code: "CONVERSATION_NOT_FOUND", Message: "会话不存在或无权限"}
ErrNotConversationMember = &AppError{Code: "NOT_CONVERSATION_MEMBER", Message: "不是会话参与者"}
ErrMessageNotFound = &AppError{Code: "MESSAGE_NOT_FOUND", Message: "消息不存在"}
ErrMessageAlreadyRecalled = &AppError{Code: "MESSAGE_ALREADY_RECALLED", Message: "消息已撤回"}
ErrMessageRecallTimeout = &AppError{Code: "MESSAGE_RECALL_TIMEOUT", Message: "消息撤回超时2分钟"}
ErrCannotSendMessage = &AppError{Code: "CANNOT_SEND_MESSAGE", Message: "无法发送消息"}
ErrCannotSendImage = &AppError{Code: "CANNOT_SEND_IMAGE", Message: "对方未关注你,暂不支持发送图片"}
ErrMessageLimitReached = &AppError{Code: "MESSAGE_LIMIT_REACHED", Message: "对方未关注你前,仅允许发送一条消息"}
ErrQRCodeSessionExpired = &AppError{Code: "QRCODE_SESSION_EXPIRED", Message: "二维码已过期"}
ErrQRCodeAlreadyScanned = &AppError{Code: "QRCODE_ALREADY_SCANNED", Message: "二维码已被扫描"}
ErrQRCodeInvalidStatus = &AppError{Code: "QRCODE_INVALID_STATUS", Message: "二维码状态无效"}
ErrPushNotImplemented = &AppError{Code: "PUSH_NOT_IMPLEMENTED", Message: "推送服务未实现"}
ErrPushQueueFull = &AppError{Code: "PUSH_QUEUE_FULL", Message: "推送队列已满"}
ErrUserOffline = &AppError{Code: "USER_OFFLINE", Message: "用户不在线"}
// 学习资料相关错误
ErrSubjectHasMaterials = &AppError{Code: "SUBJECT_HAS_MATERIALS", Message: "学科下存在资料,无法删除"}
// 帖子创建幂等性:同一 client_request_id 的请求仍在处理中(占位未回填真实结果)
ErrDuplicatePostRequest = &AppError{Code: "DUPLICATE_POST_REQUEST", Message: "正在发布中,请稍候"}
// 身份认证相关错误
ErrVerificationPending = &AppError{Code: "VERIFICATION_PENDING", Message: "已有待审核的认证申请,请等待审核"}
ErrVerificationNotFound = &AppError{Code: "VERIFICATION_NOT_FOUND", Message: "认证记录不存在"}
ErrInvalidIdentity = &AppError{Code: "INVALID_IDENTITY", Message: "无效的身份类型"}
ErrAlreadyVerified = &AppError{Code: "ALREADY_VERIFIED", Message: "已完成身份认证,无需重复申请"}
ErrVerificationAlreadyHandled = &AppError{Code: "VERIFICATION_ALREADY_HANDLED", Message: "该认证申请已被处理"}
ErrVerificationRequired = &AppError{Code: "VERIFICATION_REQUIRED", Message: "需要完成身份认证"}
// 认证 / 令牌相关错误(统一由认证管道返回)
ErrInvalidToken = &AppError{Code: "INVALID_TOKEN", Message: "令牌无效"}
ErrTokenExpired = &AppError{Code: "TOKEN_EXPIRED", Message: "令牌已过期"}
ErrSessionRevoked = &AppError{Code: "SESSION_REVOKED", Message: "会话已失效,请重新登录"}
ErrAccountInactive = &AppError{Code: "ACCOUNT_INACTIVE", Message: "账号未激活"}
ErrWrongTokenType = &AppError{Code: "WRONG_TOKEN_TYPE", Message: "令牌类型错误"}
// 初始化相关错误
ErrSetupAlreadyCompleted = &AppError{Code: "SETUP_ALREADY_COMPLETED", Message: "超级管理员已初始化,无法重复设置"}
ErrInvalidSetupSecret = &AppError{Code: "INVALID_SETUP_SECRET", Message: "初始化密钥无效"}
ErrSetupSecretNotConfigured = &AppError{Code: "SETUP_SECRET_NOT_CONFIGURED", Message: "未配置初始化密钥,无法使用此接口"}
)
// Wrap 包装错误
func Wrap(err error, appErr *AppError) *AppError {
return &AppError{
Code: appErr.Code,
Message: appErr.Message,
Cause: err,
}
}
// As 将错误转换为 AppError
func As(err error) (*AppError, bool) {
var appErr *AppError
if errors.As(err, &appErr) {
return appErr, true
}
return nil, false
}