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

@@ -27,6 +27,8 @@ export interface User {
nickname: string;
avatar: string;
role: string;
// 板块管理员被授权的板块(仅 /api/me 返回;前端据此渲染审核入口/按钮)
board_ids?: number[];
// 仅 /api/me 与 /api/profile 响应中返回(用户自身敏感信息)
email?: string;
signature?: string;
@@ -215,6 +217,7 @@ export interface UserProfile {
avatar: string;
signature: string;
role: string;
board_ids?: number[];
created_at: string;
};
stats: {
@@ -248,7 +251,7 @@ export interface NotificationItem {
id: number;
user_id: number;
actor_id: number;
type: "comment" | "reply" | "like";
type: "comment" | "reply" | "like" | "approved" | "rejected";
post_id: number;
comment_id: number;
content: string;
@@ -411,9 +414,16 @@ export async function fetchPostDetail(id: string, cookieHeader?: string): Promis
return res.json();
}
export async function fetchComments(postId: string, page = 1): Promise<CommentsResponse> {
export async function fetchComments(
postId: string,
page = 1,
cookieHeader?: string
): Promise<CommentsResponse> {
const params = new URLSearchParams({ page: String(page), size: "20" });
const res = await fetch(`${API_BASE}/api/posts/${postId}/comments?${params}`, ssrInit());
const res = await fetch(
`${API_BASE}/api/posts/${postId}/comments?${params}`,
ssrInit(cookieHeaders(cookieHeader))
);
if (!res.ok) throw new Error("获取评论失败");
return res.json();
}
@@ -784,7 +794,7 @@ export async function apiAdminDeleteAnnouncement(id: number): Promise<void> {
// ===== 用户管理(管理员) =====
// 后台用户列表项:email/最近活跃仅管理员接口返回
// 后台用户列表项:email/最近活跃/登录 IP 仅管理员接口返回
export interface AdminUser {
id: number;
username: string;
@@ -792,12 +802,33 @@ export interface AdminUser {
email: string;
avatar: string;
signature: string;
role: "user" | "admin" | string;
role: string; // user | board_admin | admin | super_admin | owner
board_ids: number[]; // 板块管理员的授权板块
banned: boolean;
post_count: number;
comment_count: number;
created_at: string;
last_seen_at: string | null;
online: boolean; // last_seen_at 在 5 分钟内
last_login_ip: string;
last_login_at: string | null;
}
// 登录历史记录
export interface LoginLog {
id: number;
user_id: number;
username: string;
ip: string;
user_agent: string;
success: boolean;
created_at: string;
}
export interface LoginLogsResponse {
logs: LoginLog[];
total: number;
page: number;
}
export interface AdminUserSummary {
@@ -846,6 +877,19 @@ export async function fetchAdminUsers(
return res.json();
}
// SSR 待审核帖子首页(转发管理团队 cookie 直连后端);权限不足时抛错由页面层处理
export async function fetchAdminPendingPosts(
page = 1,
cookieHeader?: string
): Promise<{ posts: Post[]; total: number; page: number }> {
const res = await fetch(
`${API_BASE}/api/admin/moderation/pending-posts?page=${page}`,
ssrInit(cookieHeaders(cookieHeader))
);
if (!res.ok) throw new Error("获取待审核帖子失败");
return res.json();
}
// 客户端后台用户列表(走 rewrite,携带 cookie + CSRF)
export async function apiAdminListUsers(query: AdminUsersQuery = {}): Promise<AdminUsersResponse> {
const res = await fetchWithRefresh(`/api/admin/users?${adminUsersParams(query)}`, {
@@ -856,21 +900,37 @@ export async function apiAdminListUsers(query: AdminUsersQuery = {}): Promise<Ad
return data as AdminUsersResponse;
}
// 变更用户角色:role 仅接受 "user" | "admin"
// 变更用户管理角色与板块授权:
// role 接受 user/board_admin/admin/super_admin(owner 不可授予);
// board_admin 必须携带至少一个 board_ids
export async function apiAdminSetUserRole(
id: number,
role: "user" | "admin"
role: string,
boardIds?: number[]
): Promise<{ user: AdminUser }> {
const res = await fetchWithRefresh(`/api/admin/users/${id}/role`, {
method: "PUT",
headers: clientHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ role }),
body: JSON.stringify({ role, board_ids: boardIds ?? [] }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "角色更新失败");
return data as { user: AdminUser };
}
// 某用户的登录历史(IP/UA/成败)
export async function apiAdminUserLoginLogs(
id: number,
page = 1
): Promise<LoginLogsResponse> {
const res = await fetchWithRefresh(`/api/admin/users/${id}/login-logs?page=${page}`, {
headers: clientHeaders(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "获取登录历史失败");
return data as LoginLogsResponse;
}
// 封禁 / 解封用户
export async function apiAdminSetUserBan(
id: number,
@@ -905,3 +965,80 @@ export async function apiUpdateSiteSettings(accent: string): Promise<PublicSetti
if (!res.ok) throw new Error(data.error || "保存设置失败");
return { accent: data.accent ?? "" };
}
// ===== 板块(客户端,供角色授权弹窗勾选) =====
export async function apiListBoards(): Promise<{ boards: Board[] }> {
const res = await fetchWithRefresh("/api/boards", { headers: clientHeaders() });
if (!res.ok) throw new Error("获取板块失败");
return res.json();
}
// ===== 内容审核队列 =====
export interface PendingCommentItem {
id: number;
post_id: number;
post_title: string;
board_id: number;
board: Board;
content: string;
created_at: string;
user: User;
}
export interface PendingCounts {
posts: number;
comments: number;
}
export async function apiAdminPendingPosts(page = 1): Promise<{
posts: Post[];
total: number;
page: number;
}> {
const res = await fetchWithRefresh(`/api/admin/moderation/pending-posts?page=${page}`, {
headers: clientHeaders(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "获取待审帖子失败");
return data;
}
export async function apiAdminPendingComments(page = 1): Promise<{
comments: PendingCommentItem[];
total: number;
page: number;
}> {
const res = await fetchWithRefresh(`/api/admin/moderation/pending-comments?page=${page}`, {
headers: clientHeaders(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "获取待审评论失败");
return data;
}
export async function apiAdminPendingCounts(): Promise<PendingCounts> {
const res = await fetchWithRefresh("/api/admin/moderation/counts", {
headers: clientHeaders(),
});
if (!res.ok) return { posts: 0, comments: 0 };
return res.json();
}
async function moderationAction(url: string): Promise<void> {
const res = await fetchWithRefresh(url, { method: "PUT", headers: clientHeaders() });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || "操作失败");
}
}
export const apiAdminApprovePost = (id: number) =>
moderationAction(`/api/admin/moderation/posts/${id}/approve`);
export const apiAdminRejectPost = (id: number) =>
moderationAction(`/api/admin/moderation/posts/${id}/reject`);
export const apiAdminApproveComment = (id: number) =>
moderationAction(`/api/admin/moderation/comments/${id}/approve`);
export const apiAdminRejectComment = (id: number) =>
moderationAction(`/api/admin/moderation/comments/${id}/reject`);