完善角色与消息权限:收紧板管内容处置,彻底删除仅站长,并统一管理删帖/评弹窗。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-20 14:30:08 +08:00
parent 1f2e4b2a33
commit c0462a3500
37 changed files with 1340 additions and 472 deletions

View File

@@ -44,6 +44,10 @@ export interface User {
role: string;
// 板块管理员被授权的板块(仅 /api/me 返回;前端据此渲染审核入口/按钮)
board_ids?: number[];
// 站点消息管理(站长授予;站长/超管固有监管不必依赖此字段)
can_manage_messages?: boolean;
// 作为群管理员的群数量(/me;用于后台消息入口)
chat_admin_room_count?: number;
// 仅 /api/me 与 /api/profile 响应中返回(用户自身敏感信息)
email?: string;
signature?: string;
@@ -1900,6 +1904,7 @@ export interface AdminUser {
role: string; // user | board_admin | admin | super_admin | owner
board_ids: number[]; // 板块管理员的授权板块
banned: boolean;
can_manage_messages?: boolean;
points?: number;
post_count: number;
comment_count: number;
@@ -2026,6 +2031,21 @@ export async function apiAdminSetUserRole(
return data as { user: AdminUser };
}
// 站长授予/撤销站点消息管理权限
export async function apiAdminSetUserMessages(
id: number,
canManageMessages: boolean
): Promise<{ user: AdminUser }> {
const res = await fetchWithRefresh(`/api/admin/users/${id}/messages`, {
method: "PUT",
headers: clientHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ can_manage_messages: canManageMessages }),
});
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,
@@ -2546,7 +2566,7 @@ export interface ChatRoom {
// 列表/详情聚合视图:扁平房间 + 当前访问者相关字段
export interface ChatRoomView extends ChatRoom {
joined: boolean;
my_role: "" | "owner" | "member" | "overseer";
my_role: "" | "owner" | "admin" | "member" | "overseer";
unread_count: number;
pinned?: boolean;
pin_forced?: boolean;
@@ -2558,7 +2578,7 @@ export interface ChatRoomMember {
id: number;
room_id: number;
user_id: number;
role: "owner" | "member";
role: "owner" | "admin" | "member";
last_read_message_id: number;
muted: boolean;
created_at: string;
@@ -2824,7 +2844,7 @@ export async function apiListChatMembers(
return data as ChatMembersResponse;
}
// 客户端:群主踢人
// 客户端:群主/群管踢人
export async function apiKickChatMember(
roomId: number,
userId: number
@@ -2838,7 +2858,45 @@ export async function apiKickChatMember(
return data as { ok: boolean };
}
// 客户端:邀请成员(用户名列表,群主/监管)
// 客户端:禁言/解禁
export async function apiMuteChatMember(
roomId: number,
userId: number,
muted: boolean
): Promise<{ ok: boolean; muted: boolean }> {
const res = await fetchWithRefresh(
`/api/chat/rooms/${roomId}/members/${userId}/mute`,
{
method: "PUT",
headers: clientHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ muted }),
}
);
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "禁言操作失败");
return data as { ok: boolean; muted: boolean };
}
// 客户端:站长任命/撤销群管理员
export async function apiSetChatMemberRole(
roomId: number,
userId: number,
role: "admin" | "member"
): Promise<{ ok: boolean; role: string }> {
const res = await fetchWithRefresh(
`/api/chat/rooms/${roomId}/members/${userId}/role`,
{
method: "PUT",
headers: clientHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ role }),
}
);
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "设置群角色失败");
return data as { ok: boolean; role: string };
}
// 客户端:邀请成员(用户名列表,群主/群管/监管)
export async function apiInviteChatMembers(
roomId: number,
usernames: string[]

View File

@@ -44,15 +44,23 @@ export function isOwner(role?: string | null): boolean {
return role === ROLES.OWNER;
}
/** 站长/超管:群聊监管旁路(可见全部群、撤回任意消息) */
/** 站长/超管:群聊监管旁路(可见全部群含私聊、撤回任意消息) */
export function canOverseeChat(role?: string | null): boolean {
return role === ROLES.OWNER || role === ROLES.SUPER_ADMIN;
}
/** 携带板块授权的最小用户形状(/me、帖子详情作者等均满足) */
/** 彻底删除帖/评:仅站长 */
export function canPurgeContent(role?: string | null): boolean {
return role === ROLES.OWNER;
}
/** 携带板块授权与消息权限的最小用户形状 */
export interface RoleUserLike {
role?: string | null;
board_ids?: number[] | null;
can_manage_messages?: boolean | null;
/** 是否至少管理一个群(/me 可带;后台入口用) */
chat_admin_room_count?: number | null;
}
/**
@@ -69,6 +77,29 @@ export function canModerateBoard(
return (user.board_ids ?? []).includes(boardId);
}
/**
* 板管不可软删管理员及以上作者的内容;作者本人不受此限(由调用方先判断自删)
*/
export function canModerateAuthor(
actor: RoleUserLike | null | undefined,
authorRole?: string | null
): boolean {
if (!actor) return false;
if (actor.role === ROLES.BOARD_ADMIN) {
return roleLevel(authorRole) < ROLE_LEVEL[ROLES.ADMIN];
}
return true;
}
/** 是否可软删他人内容(板块范围 + 作者角色保护) */
export function canStaffSoftDelete(
actor: RoleUserLike | null | undefined,
boardId: number | null | undefined,
authorRole?: string | null
): boolean {
return canModerateBoard(actor, boardId) && canModerateAuthor(actor, authorRole);
}
/** 是否拥有任意板块的内容审核权(后台「内容管理」入口) */
export function canModerateAny(user: RoleUserLike | null | undefined): boolean {
if (!user) return false;
@@ -76,6 +107,20 @@ export function canModerateAny(user: RoleUserLike | null | undefined): boolean {
return user.role === ROLES.BOARD_ADMIN && (user.board_ids?.length ?? 0) > 0;
}
/** 站点级消息管理(站长/超管固有,或 can_manage_messages) */
export function hasSiteMessagePerm(user: RoleUserLike | null | undefined): boolean {
if (!user) return false;
if (canOverseeChat(user.role)) return true;
return !!user.can_manage_messages;
}
/** 后台「消息管理」入口:站点消息权或至少是一个群的管理员 */
export function canAccessAdminMessages(user: RoleUserLike | null | undefined): boolean {
if (!user) return false;
if (hasSiteMessagePerm(user)) return true;
return (user.chat_admin_room_count ?? 0) > 0;
}
/** 角色图标 key(RoleBadge 负责映射到具体图标,集中配置便于替换) */
export type RoleIconKey = "crown" | "shield-star" | "shield" | "shield-board" | "user";
@@ -102,7 +147,7 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
colorToken: "--gold",
softToken: "--gold-soft",
grantDesc:
"站点所有者:全部后台能力、群聊监管、用户内容档案;身份不可授予或降级。",
"站点所有者:全部后台能力、群聊监管、用户内容档案、彻底删除内容;身份不可授予或降级。",
},
[ROLES.SUPER_ADMIN]: {
key: ROLES.SUPER_ADMIN,
@@ -111,7 +156,7 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
colorToken: "--accent",
softToken: "--accent-soft",
grantDesc:
"用户与角色管理、封禁、外观主题、公告;全站内容审核与置顶/加精;群聊监管(可见全部会话、可撤回任意消息)。不可动站长账号,亦无站长专属的用户内容档案。",
"用户与角色管理、封禁、外观主题、公告;全站内容软删与置顶/加精;群聊监管(可见全部会话、可撤回任意消息)。不可彻底删除内容,不可动站长账号,亦无站长专属的用户内容档案。",
},
[ROLES.ADMIN]: {
key: ROLES.ADMIN,
@@ -120,7 +165,7 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
colorToken: "--accent",
softToken: "--accent-soft",
grantDesc:
"公告管理;全站帖子/评论审核;前台置顶与加精。不含用户权限、外观设置与群聊监管旁路。",
"公告管理;全站帖子/评论审核与软删;前台置顶与加精。不含用户权限、外观设置、彻底删除与默认的群聊监管(可由站长另授消息管理或任命群管理员)。",
},
[ROLES.BOARD_ADMIN]: {
key: ROLES.BOARD_ADMIN,
@@ -129,7 +174,7 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
colorToken: "--accent",
softToken: "--accent-soft",
grantDesc:
"仅审核并管理被授权板块内的帖子与评论(含删除违规内容)。无公告、置顶/加精、用户管理与外观设置;须至少勾选一个板块。",
"仅审核并软删被授权板块内普通用户的帖子与评论;不可处置管理员/站长发布的内容,无消息管理、公告、置顶/加精、用户管理与外观设置;须至少勾选一个板块。",
},
[ROLES.USER]: {
key: ROLES.USER,