用户角色与权限
基于 app_metadata.role 的三级角色体系与 RLS 权限检查
角色体系
HanabiManga 使用三级角色,存储在 auth.users.app_metadata.role 中:
| 角色 | 说明 | 来源 |
|---|---|---|
user | 普通用户,注册时默认角色 | 注册自动赋予 |
editor | 内容编辑,可上传和管理漫画 | admin 通过 manage-user-role 提升 |
admin | 系统管理员,拥有全部权限 | 直接操作数据库赋予 |
角色信息在 app_metadata 中,用户自身无法修改(app_metadata 只能通过 Supabase Admin API 写入)。角色变更通过 manage-user-role Edge Function 执行,需 admin 操作。
权限检查方式
RPC 函数
项目提供两个 RPC 函数,从当前请求的 JWT 中读取角色信息:
CREATE FUNCTION public.is_admin() RETURNS boolean AS $$
DECLARE
user_role text;
BEGIN
user_role := (auth.jwt() -> 'app_metadata' ->> 'role');
RETURN user_role = 'admin';
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;严格检查 role = 'admin',editor 不通过。
CREATE FUNCTION public.is_admin_or_editor() RETURNS boolean AS $$
DECLARE
user_role text;
BEGIN
user_role := (auth.jwt() -> 'app_metadata' ->> 'role');
RETURN user_role IN ('admin', 'editor');
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;admin 和 editor 均通过。
在 RLS 策略中使用
-- 所有人可读,仅 admin/editor 可写
CREATE POLICY "Public read comics" ON public.comics
FOR SELECT USING (true);
CREATE POLICY "Admin or editor can insert comics" ON public.comics
FOR INSERT WITH CHECK (is_admin_or_editor());
CREATE POLICY "Admin or editor can update comics" ON public.comics
FOR UPDATE USING (is_admin_or_editor());
-- 仅 admin 可删
CREATE POLICY "Only admin can delete comics" ON public.comics
FOR DELETE USING (is_admin());在 Edge Function 中使用
Edge Function 有两种方式检查角色:
使用用户自己的 JWT,调用 RPC 函数检查(推荐):
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
)
const { data: isAdmin } = await supabase.rpc('is_admin')
if (!isAdmin) {
return new Response('Forbidden', { status: 403 })
}使用 SERVICE_ROLE_KEY 获取用户信息后直接读取 app_metadata:
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { data: { user } } = await supabaseAdmin.auth.getUser(token)
if (user?.app_metadata?.role !== 'admin') {
return new Response('Forbidden', { status: 403 })
}此方式用于 admin-create-user / manage-user-role 等管理函数。
在客户端检查
客户端可直接从 JWT session 中读取角色,用于 UI 展示控制(非安全边界):
const { data: { session } } = await supabase.auth.getSession()
const role = session?.user?.app_metadata?.role ?? 'user'
// UI 层控制
const canEdit = role === 'admin' || role === 'editor'
const canManageUsers = role === 'admin'客户端角色检查仅用于 UI 展示(隐藏/显示按钮)。真正的权限边界在 RLS 策略和 Edge Function 中,客户端无法绕过。
角色变更
提升 / 降级(admin 操作)
通过 manage-user-role Edge Function:
// promote: user → editor
await supabase.functions.invoke('manage-user-role', {
body: {
target_user_id: 'uuid',
action: 'promote',
reason: '负责韩漫区上传',
},
})
// demote: editor → user
await supabase.functions.invoke('manage-user-role', {
body: {
target_user_id: 'uuid',
action: 'demote',
reason: '编辑职责移交',
},
})不能通过此函数创建或降级 admin。admin 角色需直接操作数据库的 auth.users.raw_app_meta_data 字段。
审计
所有角色变更操作会记录到 admin_logs 表,包含操作者、目标用户、变更前后角色和理由。详见 用户管理函数 - 审计日志。
各角色权限一览
| 能力 | user | editor | admin |
|---|---|---|---|
| 阅读漫画 | ✓ | ✓ | ✓ |
| 发表评论 | ✓ | ✓ | ✓ |
| 自定义头像/Banner(VIP) | ✓ | ✓ | ✓ |
| 上传漫画章节 | ✓ | ✓ | |
| 编辑漫画元数据 | ✓ | ✓ | |
| 获取 Rclone 配置 | ✓ | ✓ | |
| 创建用户 | ✓ | ||
| 重置密码 | ✓ | ||
| 角色管理 | ✓ |