-- ===================================================== -- 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;