feat: 封禁用户允许登录但禁止写操作
- Session 结构体新增 Status 字段,Create/Validate 同步状态 - AuthService.Login 移除封禁检查,允许封禁用户登录 - 新增 BannedWriteGuard 中间件,拦截 POST/PUT/DELETE 写操作 - 封禁用户自动签到跳过 - 所有需登录的 API 路由组应用 BannedWriteGuard - AdminService.UpdateUserStatus 保留 DestroyByUID 确保状态刷新
This commit is contained in:
@ -51,8 +51,6 @@ func (ac *AuthController) Login(c *gin.Context) {
|
||||
switch err {
|
||||
case common.ErrInvalidCred:
|
||||
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
|
||||
case common.ErrUserBanned:
|
||||
common.Error(c, http.StatusForbidden, "账号已被封禁")
|
||||
case common.ErrUserLocked:
|
||||
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
|
||||
case common.ErrMaintenanceMode:
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/config"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
"metazone.cc/metalab/internal/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -86,10 +87,14 @@ func (am *AuthMiddleware) Optional() gin.HandlerFunc {
|
||||
// tryAutoCheckIn 尝试自动签到,并同步 session Exp 与 DB(静默失败,不影响主流程)
|
||||
// CheckIn 即使已签到也会返回 DB 中最新的 Exp,这里始终与 session 比较,
|
||||
// 确保 CompleteTask 等非签到路径的 Exp 变化也能同步到 session,避免 NAV 显示旧等级
|
||||
// 被封禁用户跳过自动签到
|
||||
func (am *AuthMiddleware) tryAutoCheckIn(c *gin.Context, s *session.Session) {
|
||||
if am.levelSvc == nil || s == nil {
|
||||
return
|
||||
}
|
||||
if s.Status == model.StatusBanned {
|
||||
return
|
||||
}
|
||||
_, newExp, _, _ := am.levelSvc.CheckIn(s.UserID)
|
||||
// 只允许 Exp 只增不减:DB 异常返回 0 或主从延迟读到旧值时不会回退
|
||||
if newExp > s.Exp {
|
||||
@ -109,6 +114,31 @@ func injectSessionContext(c *gin.Context, s *session.Session) {
|
||||
c.Set("username", s.Username)
|
||||
c.Set("avatar", s.Avatar)
|
||||
c.Set("role", s.Role)
|
||||
c.Set("status", s.Status)
|
||||
c.Set("exp", s.Exp)
|
||||
c.Set("sid", s.ID)
|
||||
}
|
||||
|
||||
// BannedWriteGuard 封禁用户写操作守卫
|
||||
// 仅拦截 POST/PUT/DELETE/PATCH 请求,GET/HEAD/OPTIONS 放行
|
||||
// 需在 Required() 之后调用,确保上下文中已有用户 status
|
||||
func (am *AuthMiddleware) BannedWriteGuard() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 只检查写操作
|
||||
switch c.Request.Method {
|
||||
case "GET", "HEAD", "OPTIONS":
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
status, _ := c.Get("status")
|
||||
if status == model.StatusBanned {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "账号已被封禁,无法执行此操作",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
adminAPI := r.Group("/api/admin")
|
||||
adminAPI.Use(d.authMdw.Required())
|
||||
adminAPI.Use(middleware.RequireMinRole(model.RoleAdmin))
|
||||
adminAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
adminAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
adminAPI.GET("/users", d.adminCtrl.ListUsers)
|
||||
@ -48,6 +49,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
siteSettingsAPI := r.Group("/api/admin/site-settings")
|
||||
siteSettingsAPI.Use(d.authMdw.Required())
|
||||
siteSettingsAPI.Use(middleware.RequireMinRole(model.RoleOwner))
|
||||
siteSettingsAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
siteSettingsAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
siteSettingsAPI.GET("", d.siteSettingCtrl.GetSettings)
|
||||
@ -60,6 +62,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
adminPostAPI := r.Group("/api/admin/posts")
|
||||
adminPostAPI.Use(d.authMdw.Required())
|
||||
adminPostAPI.Use(middleware.RequireMinRole(model.RoleAdmin))
|
||||
adminPostAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
adminPostAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
adminPostAPI.POST("/:id/approve", d.adminPostCtrl.Approve)
|
||||
|
||||
@ -12,6 +12,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
// --- 设置页 API(需登录) ---
|
||||
settingsAPI := r.Group("/api/settings")
|
||||
settingsAPI.Use(d.authMdw.Required())
|
||||
settingsAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
settingsAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
settingsAPI.PUT("/profile", d.settingsCtrl.UpdateProfile)
|
||||
@ -29,6 +30,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
// --- 消息中心 API(需登录) ---
|
||||
messagesAPI := r.Group("/api/messages")
|
||||
messagesAPI.Use(d.authMdw.Required())
|
||||
messagesAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
messagesAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
messagesAPI.GET("/unread", d.messageCtrl.UnreadCount)
|
||||
@ -39,6 +41,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
// --- 积分/等级 API(需登录) ---
|
||||
levelAPI := r.Group("/api/level")
|
||||
levelAPI.Use(d.authMdw.Required())
|
||||
levelAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
levelAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
levelAPI.POST("/checkin", d.levelCtrl.CheckIn)
|
||||
@ -64,6 +67,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
// --- 帖子 API(需登录) ---
|
||||
postsAPI := r.Group("/api/posts")
|
||||
postsAPI.Use(d.authMdw.Required())
|
||||
postsAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
postsAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
postsAPI.POST("/:id/submit", d.postCtrl.Submit)
|
||||
@ -73,6 +77,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
// --- 创作中心 API(需登录) ---
|
||||
studioAPI := r.Group("/api/studio")
|
||||
studioAPI.Use(d.authMdw.Required())
|
||||
studioAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
studioAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
studioAPI.GET("/overview", d.studioCtrl.OverviewAPI)
|
||||
|
||||
@ -148,12 +148,6 @@ func (s *AuthService) Login(req model.LoginRequest, loginIP string) (*model.User
|
||||
return nil, common.ErrUserLocked
|
||||
}
|
||||
|
||||
// 封禁
|
||||
if user.Status == model.StatusBanned {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return nil, common.ErrUserBanned
|
||||
}
|
||||
|
||||
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) != nil {
|
||||
return nil, common.ErrInvalidCred
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string
|
||||
Username: user.Username,
|
||||
Avatar: user.Avatar,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
Exp: user.Exp,
|
||||
RememberMe: rememberMe,
|
||||
IP: ip,
|
||||
@ -54,7 +55,7 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string
|
||||
}
|
||||
|
||||
// Validate 验证会话:读取 → 检查过期 → 检查用户状态 → 续期 → 返回
|
||||
// 返回 nil 表示会话无效(不存在/过期/用户被封禁)
|
||||
// 若用户不存在则返回 nil;被封禁/锁定用户仍返回 session(由上层中间件决定可访问范围)
|
||||
func (m *Manager) Validate(sid string) (*Session, error) {
|
||||
if sid == "" {
|
||||
return nil, nil
|
||||
@ -65,7 +66,7 @@ func (m *Manager) Validate(sid string) (*Session, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 检查用户是否仍然有效(封禁/注销后拒绝已有会话)
|
||||
// 检查用户是否仍然存在
|
||||
user, err := m.userRepo.FindByIDForAuth(s.UserID)
|
||||
if err != nil {
|
||||
log.Printf("[SessionManager] Validate: user not found uid=%d err=%v", s.UserID, err)
|
||||
@ -73,17 +74,11 @@ func (m *Manager) Validate(sid string) (*Session, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 封禁用户直接拒绝
|
||||
if user.Status == model.StatusBanned {
|
||||
log.Printf("[SessionManager] Validate: user banned uid=%d", s.UserID)
|
||||
m.store.Delete(sid)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 同步 DB 最新字段(头像/用户名/角色/经验值等可能已变更)
|
||||
// 同步 DB 最新字段(头像/用户名/角色/状态/经验值等可能已变更)
|
||||
s.Avatar = user.Avatar
|
||||
s.Username = user.Username
|
||||
s.Role = user.Role
|
||||
s.Status = user.Status
|
||||
s.Exp = user.Exp
|
||||
|
||||
// 滑动窗口续期
|
||||
|
||||
@ -14,6 +14,7 @@ type Session struct {
|
||||
Username string // 用户名
|
||||
Avatar string // 头像 URL
|
||||
Role string // 角色
|
||||
Status string // 账号状态(active/banned/deleted/locked)
|
||||
Exp int // 经验值(用于前端实时展示等级)
|
||||
RememberMe bool // 是否持久化(决定 cookie maxAge)
|
||||
IP string // 登录 IP
|
||||
|
||||
Reference in New Issue
Block a user