feat: 实现完整的板块级RBAC内容审核系统

- 新增站长/超级管理员/管理员/板块管理员四级角色体系
- 实现实时权限快照加载与细粒度权限校验
- 新增内容审核队列与前后端页面
- 重构用户权限管理与站点管理逻辑
- 新增评论/帖子审核状态与通知推送
- 优化前端权限控制与角色徽章展示
- 修正数据库迁移与默认值问题
This commit is contained in:
2026-09-15 04:47:34 +08:00
parent 6da4309453
commit a7b6421840
44 changed files with 3149 additions and 585 deletions

View File

@@ -17,10 +17,11 @@ var (
)
// setAuthCookies 设置认证 cookie:
// - access token(HttpOnly,15min)
// - refresh token(HttpOnly,7天,Path=/,供 Next middleware 在页面/RSC
// 请求中读取并静默轮转;仅 /api/auth/refresh 端点消费)
// - CSRF token(JS 可读,7天,双提交校验)
// - access token(HttpOnly,15min)
// - refresh token(HttpOnly,7天,Path=/,供 Next middleware 在页面/RSC
// 请求中读取并静默轮转;仅 /api/auth/refresh 端点消费)
// - CSRF token(JS 可读,7天,双提交校验)
//
// SameSite=Lax:允许外站顶级链接进入时保留登录态(Strict 会导致从外站
// 跳转进来的第一次请求丢 cookie,把已登录用户误判为游客);状态变更请求
// 另有 CSRF 双提交 token 兜底。生产 HTTPS 下 cookie 名带 __Host- 前缀。
@@ -127,6 +128,7 @@ func (h *Handlers) Register(c *gin.Context) {
return
}
setAuthCookies(c, accessToken, refreshToken, !h.Cfg.DevMode)
h.AdminUser.RecordLogin(loginUser.ID, req.Username, c.ClientIP(), c.Request.UserAgent(), true)
c.JSON(http.StatusOK, gin.H{
"user": gin.H{
"id": loginUser.ID,
@@ -147,6 +149,11 @@ func (h *Handlers) Login(c *gin.Context) {
}
accessToken, refreshToken, user, err := h.Auth.Login(req.Username, req.Password)
if err != nil {
// 登录失败也留痕:用户存在时带 user_id(封禁/错密),不存在时为 0
h.AdminUser.RecordLogin(
h.Auth.GetUserIDByUsername(req.Username), req.Username,
c.ClientIP(), c.Request.UserAgent(), false,
)
// 封禁与凭据错误区分:前端可据此展示针对性提示
if errors.Is(err, service.ErrAccountBanned) {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error(), "code": "account_banned"})
@@ -155,6 +162,8 @@ func (h *Handlers) Login(c *gin.Context) {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
// 登录成功留痕(IP/UA/时间)
h.AdminUser.RecordLogin(user.ID, req.Username, c.ClientIP(), c.Request.UserAgent(), true)
// dev 模式不设 Secure,生产环境需 HTTPS
setAuthCookies(c, accessToken, refreshToken, !h.Cfg.DevMode)
c.JSON(http.StatusOK, gin.H{
@@ -232,8 +241,12 @@ func (h *Handlers) ChangePassword(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "密码修改成功,请重新登录"})
}
// meUserBody 统一的用户信息响应体(Me/Login/Refresh 共用形状)
func meUserBody(user *model.User) gin.H {
// meUserBody 统一的用户信息响应体。boardIDs 为板块管理员被授权的板块,
// 供前端渲染管理菜单与前台审核按钮;其他角色为空数组
func meUserBody(user *model.User, boardIDs []uint) gin.H {
if boardIDs == nil {
boardIDs = []uint{}
}
return gin.H{
"id": user.ID,
"username": user.Username,
@@ -242,6 +255,7 @@ func meUserBody(user *model.User) gin.H {
"signature": user.Signature,
"email": user.Email,
"role": user.Role,
"board_ids": boardIDs,
}
}
@@ -268,5 +282,10 @@ func (h *Handlers) Me(c *gin.Context) {
if n, err := h.Notification.UnreadCount(claims.ID); err == nil {
unread = n
}
c.JSON(http.StatusOK, gin.H{"user": meUserBody(user), "unread_count": unread})
// 板块管理员带出授权板块,供前端菜单/按钮按板块范围渲染
var boardIDs []uint
if user.Role == model.RoleBoardAdmin {
boardIDs, _ = h.Auth.GetUserBoardIDs(claims.ID)
}
c.JSON(http.StatusOK, gin.H{"user": meUserBody(user, boardIDs), "unread_count": unread})
}