Files
akmon/supabase_auth_complete_setup.sql
2026-01-20 08:04:15 +08:00

602 lines
20 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =====================================================
-- Supabase Auth 完整角色管理与权限配置
-- 适用于教师/学生消息系统的完整解决方案
-- =====================================================
-- 清理和重置(谨慎使用)
DO $$
BEGIN
-- 删除现有触发器
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
DROP TRIGGER IF EXISTS trigger_update_user_roles_updated_at ON public.user_roles;
-- 删除现有函数
DROP FUNCTION IF EXISTS public.handle_new_user() CASCADE;
DROP FUNCTION IF EXISTS public.update_user_roles_updated_at() CASCADE;
DROP FUNCTION IF EXISTS public.update_user_role(UUID, TEXT, UUID) CASCADE;
DROP FUNCTION IF EXISTS public.get_user_role(UUID) CASCADE;
DROP FUNCTION IF EXISTS public.batch_update_user_roles(JSONB, UUID) CASCADE;
DROP FUNCTION IF EXISTS public.sync_user_role_metadata(UUID) CASCADE;
-- 删除现有视图
DROP VIEW IF EXISTS public.user_roles_with_email CASCADE;
RAISE NOTICE '🧹 已清理现有角色管理组件';
END $$;
-- =====================================================
-- 1. 核心角色管理表
-- =====================================================
-- 创建用户角色管理表
CREATE TABLE IF NOT EXISTS public.user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('admin', 'teacher', 'student')),
-- 扩展字段支持更复杂的权限管理
class_id UUID DEFAULT NULL,
school_id UUID DEFAULT NULL,
department TEXT DEFAULT NULL,
permissions JSONB DEFAULT '{}'::jsonb,
-- 状态管理
is_active BOOLEAN DEFAULT true,
expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
-- 审计字段
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id),
updated_by UUID REFERENCES auth.users(id),
-- 确保每个用户只有一个主要角色
UNIQUE(user_id)
);
-- 创建优化索引
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON public.user_roles(user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role ON public.user_roles(role);
CREATE INDEX IF NOT EXISTS idx_user_roles_class_id ON public.user_roles(class_id) WHERE class_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_user_roles_active ON public.user_roles(is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_user_roles_expires ON public.user_roles(expires_at) WHERE expires_at IS NOT NULL;
-- =====================================================
-- 2. 核心角色管理函数
-- =====================================================
-- 更新时间戳触发器函数
CREATE OR REPLACE FUNCTION public.update_user_roles_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
NEW.updated_by = COALESCE(auth.uid(), NEW.updated_by);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 创建更新触发器
CREATE TRIGGER trigger_update_user_roles_updated_at
BEFORE UPDATE ON public.user_roles
FOR EACH ROW
EXECUTE FUNCTION public.update_user_roles_updated_at();
-- 同步用户角色到 auth.users 元数据的函数
CREATE OR REPLACE FUNCTION public.sync_user_role_metadata(target_user_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
user_role_record RECORD;
BEGIN
-- 获取用户角色信息
SELECT role, class_id, school_id, department, permissions, is_active
INTO user_role_record
FROM public.user_roles
WHERE user_id = target_user_id AND is_active = true;
IF FOUND THEN
-- 更新 auth.users 的元数据
UPDATE auth.users
SET raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) ||
jsonb_build_object(
'user_role', user_role_record.role,
'class_id', user_role_record.class_id,
'school_id', user_role_record.school_id,
'department', user_role_record.department,
'permissions', user_role_record.permissions,
'role_synced_at', extract(epoch from now())
)
WHERE id = target_user_id;
RETURN true;
ELSE
-- 用户没有活跃角色,清除角色信息
UPDATE auth.users
SET raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) ||
jsonb_build_object(
'user_role', 'inactive',
'role_synced_at', extract(epoch from now())
)
WHERE id = target_user_id;
RETURN false;
END IF;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 自动为新用户分配角色的函数
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
DECLARE
user_role TEXT := 'student'; -- 默认角色
user_email TEXT := NEW.email;
user_domain TEXT;
BEGIN
-- 提取邮箱域名
user_domain := split_part(user_email, '@', 2);
-- 根据邮箱域名或特定规则分配角色
CASE
WHEN user_domain IN ('teacher.edu', 'faculty.edu', 'staff.edu') THEN
user_role := 'teacher';
WHEN user_domain IN ('admin.edu', 'management.edu') THEN
user_role := 'admin';
WHEN user_email LIKE '%admin%' OR user_email LIKE '%manager%' THEN
user_role := 'admin';
WHEN user_email LIKE '%teacher%' OR user_email LIKE '%faculty%' THEN
user_role := 'teacher';
ELSE
user_role := 'student';
END CASE;
-- 插入角色记录
INSERT INTO public.user_roles (user_id, role, created_by)
VALUES (NEW.id, user_role, NEW.id);
-- 同步到元数据
PERFORM public.sync_user_role_metadata(NEW.id);
RAISE NOTICE '✅ 为用户 % 分配角色: %', NEW.email, user_role;
RETURN NEW;
EXCEPTION WHEN OTHERS THEN
RAISE WARNING '⚠️ 为用户 % 分配角色失败: %', NEW.email, SQLERRM;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 创建新用户触发器
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();
-- =====================================================
-- 3. 角色管理和查询函数
-- =====================================================
-- 获取用户角色的函数(支持缓存)
CREATE OR REPLACE FUNCTION public.get_user_role(target_user_id UUID DEFAULT auth.uid())
RETURNS TEXT AS $$
DECLARE
user_role TEXT;
cached_role TEXT;
role_synced_at NUMERIC;
BEGIN
-- 如果没有用户ID返回匿名
IF target_user_id IS NULL THEN
RETURN 'anonymous';
END IF;
-- 尝试从 auth.users 元数据获取缓存角色
SELECT
raw_user_meta_data->>'user_role',
(raw_user_meta_data->>'role_synced_at')::numeric
INTO cached_role, role_synced_at
FROM auth.users
WHERE id = target_user_id;
-- 如果缓存新鲜5分钟内直接返回
IF cached_role IS NOT NULL AND role_synced_at IS NOT NULL
AND (extract(epoch from now()) - role_synced_at) < 300 THEN
RETURN cached_role;
END IF;
-- 从角色表获取最新角色
SELECT role INTO user_role
FROM public.user_roles
WHERE user_id = target_user_id AND is_active = true;
-- 如果找到角色,同步到元数据
IF FOUND THEN
PERFORM public.sync_user_role_metadata(target_user_id);
RETURN user_role;
ELSE
RETURN 'student'; -- 默认角色
END IF;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 更新用户角色的函数(权限检查)
CREATE OR REPLACE FUNCTION public.update_user_role(
target_user_id UUID,
new_role TEXT,
operator_user_id UUID DEFAULT auth.uid(),
additional_data JSONB DEFAULT '{}'::jsonb
)
RETURNS BOOLEAN AS $$
DECLARE
operator_role TEXT;
old_role TEXT;
BEGIN
-- 检查新角色是否有效
IF new_role NOT IN ('admin', 'teacher', 'student') THEN
RAISE EXCEPTION 'Invalid role: %. Must be admin, teacher, or student', new_role;
END IF;
-- 获取操作者角色
operator_role := public.get_user_role(operator_user_id);
-- 权限检查:只有管理员可以修改角色
IF operator_role != 'admin' THEN
RAISE EXCEPTION 'Permission denied. Only admins can update user roles';
END IF;
-- 获取旧角色
SELECT role INTO old_role FROM public.user_roles WHERE user_id = target_user_id;
-- 更新角色表
INSERT INTO public.user_roles (
user_id, role, class_id, school_id, department, permissions, updated_by
) VALUES (
target_user_id,
new_role,
COALESCE(additional_data->>'class_id', NULL)::uuid,
COALESCE(additional_data->>'school_id', NULL)::uuid,
additional_data->>'department',
COALESCE(additional_data->'permissions', '{}'::jsonb),
operator_user_id
)
ON CONFLICT (user_id) DO UPDATE SET
role = EXCLUDED.role,
class_id = COALESCE(EXCLUDED.class_id, user_roles.class_id),
school_id = COALESCE(EXCLUDED.school_id, user_roles.school_id),
department = COALESCE(EXCLUDED.department, user_roles.department),
permissions = COALESCE(EXCLUDED.permissions, user_roles.permissions),
updated_at = NOW(),
updated_by = EXCLUDED.updated_by;
-- 同步到元数据
PERFORM public.sync_user_role_metadata(target_user_id);
RAISE NOTICE '✅ 用户角色更新: % -> % (操作者: %)', old_role, new_role, operator_role;
RETURN true;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 批量角色管理函数
CREATE OR REPLACE FUNCTION public.batch_update_user_roles(
role_updates JSONB,
operator_user_id UUID DEFAULT auth.uid()
)
RETURNS TABLE(
user_id UUID,
email TEXT,
old_role TEXT,
new_role TEXT,
success BOOLEAN,
error_message TEXT
) AS $$
DECLARE
update_record RECORD;
operator_role TEXT;
old_role_val TEXT;
user_email_val TEXT;
BEGIN
-- 检查操作者权限
operator_role := public.get_user_role(operator_user_id);
IF operator_role != 'admin' THEN
RAISE EXCEPTION 'Permission denied. Only admins can batch update user roles';
END IF;
-- 处理每个更新请求
FOR update_record IN
SELECT * FROM jsonb_to_recordset(role_updates)
AS x(user_id UUID, role TEXT, class_id UUID, school_id UUID, department TEXT)
LOOP
-- 获取用户信息
SELECT ur.role, au.email INTO old_role_val, user_email_val
FROM public.user_roles ur
RIGHT JOIN auth.users au ON au.id = ur.user_id
WHERE au.id = update_record.user_id;
-- 尝试更新
BEGIN
PERFORM public.update_user_role(
update_record.user_id,
update_record.role,
operator_user_id,
jsonb_build_object(
'class_id', update_record.class_id,
'school_id', update_record.school_id,
'department', update_record.department
)
);
RETURN QUERY SELECT
update_record.user_id,
user_email_val,
old_role_val,
update_record.role,
true,
NULL::TEXT;
EXCEPTION WHEN OTHERS THEN
RETURN QUERY SELECT
update_record.user_id,
user_email_val,
old_role_val,
update_record.role,
false,
SQLERRM;
END;
END LOOP;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- 4. 权限检查和验证函数
-- =====================================================
-- 检查用户是否有特定权限
CREATE OR REPLACE FUNCTION public.user_has_permission(
permission_name TEXT,
target_user_id UUID DEFAULT auth.uid()
)
RETURNS BOOLEAN AS $$
DECLARE
user_role TEXT;
user_permissions JSONB;
BEGIN
-- 获取用户角色和权限
SELECT role, permissions INTO user_role, user_permissions
FROM public.user_roles
WHERE user_id = target_user_id AND is_active = true;
-- 管理员拥有所有权限
IF user_role = 'admin' THEN
RETURN true;
END IF;
-- 检查角色默认权限
CASE user_role
WHEN 'teacher' THEN
IF permission_name IN ('send_message', 'view_student_messages', 'create_group') THEN
RETURN true;
END IF;
WHEN 'student' THEN
IF permission_name IN ('send_message', 'view_own_messages') THEN
RETURN true;
END IF;
END CASE;
-- 检查自定义权限
IF user_permissions IS NOT NULL THEN
RETURN (user_permissions->permission_name)::boolean = true;
END IF;
RETURN false;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 检查用户是否可以访问特定资源
CREATE OR REPLACE FUNCTION public.can_access_resource(
resource_type TEXT,
resource_id UUID,
access_type TEXT DEFAULT 'read',
target_user_id UUID DEFAULT auth.uid()
)
RETURNS BOOLEAN AS $$
DECLARE
user_role TEXT;
BEGIN
user_role := public.get_user_role(target_user_id);
-- 管理员可以访问所有资源
IF user_role = 'admin' THEN
RETURN true;
END IF;
-- 根据资源类型和访问类型进行权限检查
CASE resource_type
WHEN 'message' THEN
-- 检查消息访问权限
RETURN EXISTS (
SELECT 1 FROM public.ak_messages m
WHERE m.id = resource_id
AND (
(m.sender_type = 'user' AND m.sender_id = target_user_id) OR
(m.receiver_type = 'user' AND m.receiver_id = target_user_id) OR
(m.receiver_type = 'group' AND m.receiver_id IN (
SELECT group_id FROM public.ak_message_group_members
WHERE user_id = target_user_id AND status = 'active'
)) OR
(m.receiver_type = 'broadcast')
)
);
WHEN 'group' THEN
-- 检查群组访问权限
RETURN EXISTS (
SELECT 1 FROM public.ak_message_group_members
WHERE group_id = resource_id AND user_id = target_user_id AND status = 'active'
);
ELSE
RETURN false;
END CASE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- 5. 管理视图和查询
-- =====================================================
-- 用户角色详细信息视图
CREATE OR REPLACE VIEW public.user_roles_detailed AS
SELECT
ur.id,
ur.user_id,
ur.role,
ur.class_id,
ur.school_id,
ur.department,
ur.permissions,
ur.is_active,
ur.expires_at,
ur.created_at,
ur.updated_at,
-- 用户信息
au.email,
au.email_confirmed_at,
au.last_sign_in_at,
-- 元数据同步状态
au.raw_user_meta_data->>'user_role' as metadata_role,
(au.raw_user_meta_data->>'role_synced_at')::numeric as role_synced_at,
CASE
WHEN ur.role = au.raw_user_meta_data->>'user_role' THEN true
ELSE false
END as role_synced,
-- 统计信息
(SELECT COUNT(*) FROM public.ak_messages WHERE sender_id = ur.user_id) as sent_messages,
(SELECT COUNT(*) FROM public.ak_message_recipients WHERE user_id = ur.user_id) as received_messages
FROM public.user_roles ur
LEFT JOIN auth.users au ON au.id = ur.user_id;
-- =====================================================
-- 6. RLS 策略 for user_roles 表
-- =====================================================
ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;
-- 删除现有策略(所有可能的策略名称)
DROP POLICY IF EXISTS "Users can view own role" ON public.user_roles;
DROP POLICY IF EXISTS "users_can_view_own_role" ON public.user_roles;
DROP POLICY IF EXISTS "Admins can manage all roles" ON public.user_roles;
DROP POLICY IF EXISTS "admins_can_manage_all_roles" ON public.user_roles;
DROP POLICY IF EXISTS "Teachers can view students in same class" ON public.user_roles;
DROP POLICY IF EXISTS "teachers_can_view_class_roles" ON public.user_roles;
-- 用户可以查看自己的角色
CREATE POLICY "users_can_view_own_role" ON public.user_roles
FOR SELECT USING (auth.uid() = user_id);
-- 管理员可以查看和管理所有角色
CREATE POLICY "admins_can_manage_all_roles" ON public.user_roles
FOR ALL USING (
public.get_user_role(auth.uid()) = 'admin'
);
-- 教师可以查看同班学生的角色(如果有班级系统)
CREATE POLICY "teachers_can_view_class_roles" ON public.user_roles
FOR SELECT USING (
public.get_user_role(auth.uid()) = 'teacher'
AND class_id IS NOT NULL
AND class_id IN (
SELECT class_id FROM public.user_roles teacher_role
WHERE teacher_role.user_id = auth.uid()
AND teacher_role.role = 'teacher'
)
);
-- =====================================================
-- 7. 初始化测试数据
-- =====================================================
-- 清理现有测试数据
DELETE FROM public.user_roles WHERE user_id IN (
'7bf7378e-a027-473e-97ac-3460ed3f170a',
'eed3824b-bba1-4309-8048-19d17367c084'
);
-- 插入测试用户角色
INSERT INTO public.user_roles (user_id, role, department, permissions, created_by) VALUES
(
'7bf7378e-a027-473e-97ac-3460ed3f170a', -- 教师用户ID
'teacher',
'Computer Science',
'{"can_create_groups": true, "can_send_broadcasts": false, "can_moderate": true}'::jsonb,
'7bf7378e-a027-473e-97ac-3460ed3f170a'
),
(
'eed3824b-bba1-4309-8048-19d17367c084', -- 学生用户ID
'student',
'Computer Science',
'{"can_create_groups": false, "can_send_broadcasts": false}'::jsonb,
'eed3824b-bba1-4309-8048-19d17367c084'
);
-- 同步到 auth.users 元数据
SELECT public.sync_user_role_metadata('7bf7378e-a027-473e-97ac-3460ed3f170a');
SELECT public.sync_user_role_metadata('eed3824b-bba1-4309-8048-19d17367c084');
-- =====================================================
-- 8. 使用示例和测试查询
-- =====================================================
/*
-- 示例1查询用户角色
SELECT public.get_user_role('7bf7378e-a027-473e-97ac-3460ed3f170a');
-- 示例2检查权限
SELECT public.user_has_permission('send_message', '7bf7378e-a027-473e-97ac-3460ed3f170a');
-- 示例3检查资源访问权限
SELECT public.can_access_resource('message', 'some-message-id', 'read', auth.uid());
-- 示例4更新用户角色需要管理员权限
SELECT public.update_user_role(
'eed3824b-bba1-4309-8048-19d17367c084',
'teacher',
auth.uid(),
'{"department": "Mathematics", "class_id": "some-class-id"}'::jsonb
);
-- 示例5批量更新角色
SELECT * FROM public.batch_update_user_roles(
'[
{
"user_id": "eed3824b-bba1-4309-8048-19d17367c084",
"role": "teacher",
"department": "Mathematics"
}
]'::jsonb
);
-- 示例6查看用户详细信息
SELECT * FROM public.user_roles_detailed
WHERE email LIKE '%test%'
ORDER BY created_at DESC;
-- 示例7查看角色同步状态
SELECT user_id, email, role, metadata_role, role_synced
FROM public.user_roles_detailed
WHERE NOT role_synced OR role_synced IS NULL;
-- 示例8强制同步所有用户角色
SELECT user_id, public.sync_user_role_metadata(user_id) as synced
FROM public.user_roles;
*/
-- 完成提示
SELECT
'✅ Supabase Auth 角色管理系统配置完成' as status,
count(*) as total_roles,
count(*) FILTER (WHERE role = 'admin') as admins,
count(*) FILTER (WHERE role = 'teacher') as teachers,
count(*) FILTER (WHERE role = 'student') as students
FROM public.user_roles;