Logo花火漫画开发文档

用户角色与权限

基于 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 表,包含操作者、目标用户、变更前后角色和理由。详见 用户管理函数 - 审计日志

各角色权限一览

能力usereditoradmin
阅读漫画
发表评论
自定义头像/Banner(VIP)
上传漫画章节
编辑漫画元数据
获取 Rclone 配置
创建用户
重置密码
角色管理

On this page