-- ============================================
-- COMBINED MIGRATION: Admin + Moderation System
-- Run this script in Supabase SQL Editor
-- ============================================
-- ============================================
-- PART 1: ADMIN SETTINGS (from 008)
-- ============================================
-- Global application settings
CREATE TABLE IF NOT EXISTS public.app_settings (
id VARCHAR(50) PRIMARY KEY,
value JSONB NOT NULL DEFAULT '{}',
description TEXT,
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by UUID REFERENCES auth.users(id)
);
INSERT INTO public.app_settings (id, value, description) VALUES
('email_reminders', '{"enabled": true, "module_reminders": true, "streak_reminders": true, "comeback_reminders": true, "daily_limit": 100}', 'Global email reminder settings'),
('registration', '{"enabled": true, "require_email_verification": false}', 'User registration settings'),
('maintenance', '{"enabled": false, "message": ""}', 'Maintenance mode settings')
ON CONFLICT (id) DO NOTHING;
-- Admin users table
CREATE TABLE IF NOT EXISTS public.admin_users (
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL DEFAULT 'moderator' CHECK (role IN ('super_admin', 'admin', 'moderator')),
permissions JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID REFERENCES auth.users(id)
);
ALTER TABLE public.app_settings ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.admin_users ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Anyone can read app settings" ON public.app_settings;
CREATE POLICY "Anyone can read app settings" ON public.app_settings
FOR SELECT USING (true);
DROP POLICY IF EXISTS "Admins can modify settings" ON public.app_settings;
CREATE POLICY "Admins can modify settings" ON public.app_settings
FOR ALL USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid() AND role IN ('super_admin', 'admin'))
);
DROP POLICY IF EXISTS "Super admin can manage admins" ON public.admin_users;
CREATE POLICY "Super admin can manage admins" ON public.admin_users
FOR ALL USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid() AND role = 'super_admin')
);
DROP POLICY IF EXISTS "Admins can view admin list" ON public.admin_users;
CREATE POLICY "Admins can view admin list" ON public.admin_users
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
-- Function to check if user is admin
CREATE OR REPLACE FUNCTION public.is_admin(check_user_id UUID DEFAULT auth.uid())
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = check_user_id);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to get admin role
CREATE OR REPLACE FUNCTION public.get_admin_role(check_user_id UUID DEFAULT auth.uid())
RETURNS VARCHAR AS $$
DECLARE
user_role VARCHAR;
BEGIN
SELECT role INTO user_role FROM public.admin_users WHERE user_id = check_user_id;
RETURN user_role;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to toggle user email reminders (admin only)
CREATE OR REPLACE FUNCTION public.admin_toggle_user_reminders(
target_user_id UUID,
disabled BOOLEAN
)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
INSERT INTO public.user_notification_preferences (user_id, admin_disabled, updated_at)
VALUES (target_user_id, disabled, NOW())
ON CONFLICT (user_id) DO UPDATE SET
admin_disabled = disabled,
updated_at = NOW();
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to update global settings (admin only)
CREATE OR REPLACE FUNCTION public.admin_update_setting(
setting_id VARCHAR,
new_value JSONB
)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
UPDATE public.app_settings
SET value = new_value, updated_at = NOW(), updated_by = auth.uid()
WHERE id = setting_id;
RETURN FOUND;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- ============================================
-- PART 2: EXTENDED ADMIN FEATURES (from 009)
-- ============================================
-- Admin activity log
CREATE TABLE IF NOT EXISTS public.admin_activity_log (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
admin_id UUID NOT NULL REFERENCES auth.users(id),
action VARCHAR(100) NOT NULL,
target_type VARCHAR(50),
target_id VARCHAR(255),
details JSONB DEFAULT '{}',
ip_address INET,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_admin_activity_admin ON public.admin_activity_log(admin_id);
CREATE INDEX IF NOT EXISTS idx_admin_activity_created ON public.admin_activity_log(created_at DESC);
ALTER TABLE public.admin_activity_log ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Admins can view activity log" ON public.admin_activity_log;
CREATE POLICY "Admins can view activity log" ON public.admin_activity_log
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
DROP POLICY IF EXISTS "System can insert activity log" ON public.admin_activity_log;
CREATE POLICY "System can insert activity log" ON public.admin_activity_log
FOR INSERT WITH CHECK (true);
-- Reported content
CREATE TABLE IF NOT EXISTS public.reported_content (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
reporter_id UUID REFERENCES auth.users(id),
content_type VARCHAR(50) NOT NULL,
content_id UUID NOT NULL,
reason VARCHAR(100) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'actioned', 'dismissed')),
reviewed_by UUID REFERENCES auth.users(id),
reviewed_at TIMESTAMPTZ,
action_taken VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_reported_content_status ON public.reported_content(status);
CREATE INDEX IF NOT EXISTS idx_reported_content_created ON public.reported_content(created_at DESC);
ALTER TABLE public.reported_content ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Users can report content" ON public.reported_content;
CREATE POLICY "Users can report content" ON public.reported_content
FOR INSERT WITH CHECK (auth.uid() = reporter_id);
DROP POLICY IF EXISTS "Admins can view reports" ON public.reported_content;
CREATE POLICY "Admins can view reports" ON public.reported_content
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
DROP POLICY IF EXISTS "Admins can update reports" ON public.reported_content;
CREATE POLICY "Admins can update reports" ON public.reported_content
FOR UPDATE USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
-- User bans
CREATE TABLE IF NOT EXISTS public.user_bans (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
banned_by UUID NOT NULL REFERENCES auth.users(id),
reason TEXT NOT NULL,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
lifted_at TIMESTAMPTZ,
lifted_by UUID REFERENCES auth.users(id)
);
CREATE INDEX IF NOT EXISTS idx_user_bans_user ON public.user_bans(user_id);
CREATE INDEX IF NOT EXISTS idx_user_bans_active ON public.user_bans(user_id) WHERE lifted_at IS NULL;
ALTER TABLE public.user_bans ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Admins can manage bans" ON public.user_bans;
CREATE POLICY "Admins can manage bans" ON public.user_bans
FOR ALL USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
-- Log admin activity function
CREATE OR REPLACE FUNCTION public.log_admin_activity(
p_action VARCHAR,
p_target_type VARCHAR DEFAULT NULL,
p_target_id VARCHAR DEFAULT NULL,
p_details JSONB DEFAULT '{}'
)
RETURNS UUID AS $$
DECLARE
log_id UUID;
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
INSERT INTO public.admin_activity_log (admin_id, action, target_type, target_id, details)
VALUES (auth.uid(), p_action, p_target_type, p_target_id, p_details)
RETURNING id INTO log_id;
RETURN log_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Ban user function
CREATE OR REPLACE FUNCTION public.admin_ban_user(
p_user_id UUID,
p_reason TEXT,
p_expires_at TIMESTAMPTZ DEFAULT NULL
)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
IF EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = p_user_id) THEN
RAISE EXCEPTION 'Cannot ban admin users';
END IF;
INSERT INTO public.user_bans (user_id, banned_by, reason, expires_at)
VALUES (p_user_id, auth.uid(), p_reason, p_expires_at);
PERFORM public.log_admin_activity(
'ban_user',
'user',
p_user_id::VARCHAR,
jsonb_build_object('reason', p_reason, 'expires_at', p_expires_at)
);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Unban user function
CREATE OR REPLACE FUNCTION public.admin_unban_user(p_user_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
UPDATE public.user_bans
SET lifted_at = NOW(), lifted_by = auth.uid()
WHERE user_id = p_user_id AND lifted_at IS NULL;
PERFORM public.log_admin_activity('unban_user', 'user', p_user_id::VARCHAR);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Check if user is banned
CREATE OR REPLACE FUNCTION public.is_user_banned(p_user_id UUID DEFAULT auth.uid())
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM public.user_bans
WHERE user_id = p_user_id
AND lifted_at IS NULL
AND (expires_at IS NULL OR expires_at > NOW())
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Review reported content
CREATE OR REPLACE FUNCTION public.admin_review_report(
p_report_id UUID,
p_status VARCHAR,
p_action_taken VARCHAR DEFAULT NULL
)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
UPDATE public.reported_content
SET
status = p_status,
reviewed_by = auth.uid(),
reviewed_at = NOW(),
action_taken = p_action_taken
WHERE id = p_report_id;
PERFORM public.log_admin_activity(
'review_report',
'report',
p_report_id::VARCHAR,
jsonb_build_object('status', p_status, 'action', p_action_taken)
);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- ============================================
-- PART 3: CONTENT MODERATION (from 011)
-- ============================================
-- Banned words table
CREATE TABLE IF NOT EXISTS public.banned_words (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
word VARCHAR(100) NOT NULL,
locale VARCHAR(5) DEFAULT NULL,
severity VARCHAR(20) DEFAULT 'medium' CHECK (severity IN ('low', 'medium', 'high')),
is_regex BOOLEAN DEFAULT FALSE,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(word, locale)
);
CREATE INDEX IF NOT EXISTS idx_banned_words_locale ON public.banned_words(locale);
CREATE INDEX IF NOT EXISTS idx_banned_words_severity ON public.banned_words(severity);
ALTER TABLE public.banned_words ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Admins can manage banned words" ON public.banned_words;
CREATE POLICY "Admins can manage banned words" ON public.banned_words
FOR ALL USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
-- Add moderation fields to discussions (if table exists)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'discussions') THEN
ALTER TABLE public.discussions
ADD COLUMN IF NOT EXISTS is_hidden BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS hidden_reason TEXT,
ADD COLUMN IF NOT EXISTS hidden_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS hidden_by UUID REFERENCES auth.users(id),
ADD COLUMN IF NOT EXISTS auto_moderated BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS triggered_word VARCHAR(100);
END IF;
END $$;
-- Add moderation fields to discussion_replies (if table exists)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'discussion_replies') THEN
ALTER TABLE public.discussion_replies
ADD COLUMN IF NOT EXISTS is_hidden BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS hidden_reason TEXT,
ADD COLUMN IF NOT EXISTS hidden_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS hidden_by UUID REFERENCES auth.users(id),
ADD COLUMN IF NOT EXISTS auto_moderated BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS triggered_word VARCHAR(100);
END IF;
END $$;
-- Moderation log table
CREATE TABLE IF NOT EXISTS public.moderation_log (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
content_type VARCHAR(20) NOT NULL CHECK (content_type IN ('discussion', 'reply')),
content_id UUID NOT NULL,
action VARCHAR(50) NOT NULL,
reason TEXT,
triggered_word VARCHAR(100),
performed_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_mod_log_content ON public.moderation_log(content_type, content_id);
CREATE INDEX IF NOT EXISTS idx_mod_log_created ON public.moderation_log(created_at DESC);
ALTER TABLE public.moderation_log ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Admins can view moderation log" ON public.moderation_log;
CREATE POLICY "Admins can view moderation log" ON public.moderation_log
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
DROP POLICY IF EXISTS "System can insert moderation log" ON public.moderation_log;
CREATE POLICY "System can insert moderation log" ON public.moderation_log
FOR INSERT WITH CHECK (true);
-- View for hidden content (Admin Panel)
CREATE OR REPLACE VIEW public.admin_hidden_content AS
SELECT
'discussion' as content_type,
d.id as content_id,
d.title,
d.content,
d.author_id,
d.author_name,
d.is_hidden,
d.hidden_reason,
d.hidden_at,
d.hidden_by,
d.auto_moderated,
d.triggered_word,
d.created_at
FROM public.discussions d
WHERE d.is_hidden = TRUE
UNION ALL
SELECT
'reply' as content_type,
r.id as content_id,
NULL as title,
r.content,
r.author_id,
r.author_name,
r.is_hidden,
r.hidden_reason,
r.hidden_at,
r.hidden_by,
r.auto_moderated,
r.triggered_word,
r.created_at
FROM public.discussion_replies r
WHERE r.is_hidden = TRUE
ORDER BY hidden_at DESC NULLS LAST, created_at DESC;
-- Function: Hide content (manual moderation)
CREATE OR REPLACE FUNCTION public.admin_hide_content(
p_content_type VARCHAR,
p_content_id UUID,
p_reason TEXT
)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
IF p_content_type = 'discussion' THEN
UPDATE public.discussions
SET
is_hidden = TRUE,
hidden_reason = p_reason,
hidden_at = NOW(),
hidden_by = auth.uid(),
auto_moderated = FALSE
WHERE id = p_content_id;
ELSIF p_content_type = 'reply' THEN
UPDATE public.discussion_replies
SET
is_hidden = TRUE,
hidden_reason = p_reason,
hidden_at = NOW(),
hidden_by = auth.uid(),
auto_moderated = FALSE
WHERE id = p_content_id;
ELSE
RAISE EXCEPTION 'Invalid content type';
END IF;
INSERT INTO public.moderation_log (content_type, content_id, action, reason, performed_by)
VALUES (p_content_type, p_content_id, 'manual_hide', p_reason, auth.uid());
PERFORM public.log_admin_activity(
'hide_content',
p_content_type,
p_content_id::VARCHAR,
jsonb_build_object('reason', p_reason)
);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Unhide content
CREATE OR REPLACE FUNCTION public.admin_unhide_content(
p_content_type VARCHAR,
p_content_id UUID
)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
IF p_content_type = 'discussion' THEN
UPDATE public.discussions
SET
is_hidden = FALSE,
hidden_reason = NULL,
hidden_at = NULL,
hidden_by = NULL,
auto_moderated = FALSE,
triggered_word = NULL
WHERE id = p_content_id;
ELSIF p_content_type = 'reply' THEN
UPDATE public.discussion_replies
SET
is_hidden = FALSE,
hidden_reason = NULL,
hidden_at = NULL,
hidden_by = NULL,
auto_moderated = FALSE,
triggered_word = NULL
WHERE id = p_content_id;
ELSE
RAISE EXCEPTION 'Invalid content type';
END IF;
INSERT INTO public.moderation_log (content_type, content_id, action, performed_by)
VALUES (p_content_type, p_content_id, 'unhide', auth.uid());
PERFORM public.log_admin_activity('unhide_content', p_content_type, p_content_id::VARCHAR);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Delete content
CREATE OR REPLACE FUNCTION public.admin_delete_content(
p_content_type VARCHAR,
p_content_id UUID
)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
INSERT INTO public.moderation_log (content_type, content_id, action, performed_by)
VALUES (p_content_type, p_content_id, 'delete', auth.uid());
IF p_content_type = 'discussion' THEN
DELETE FROM public.discussions WHERE id = p_content_id;
ELSIF p_content_type = 'reply' THEN
DELETE FROM public.discussion_replies WHERE id = p_content_id;
ELSE
RAISE EXCEPTION 'Invalid content type';
END IF;
PERFORM public.log_admin_activity('delete_content', p_content_type, p_content_id::VARCHAR);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Auto-hide content (called from application)
CREATE OR REPLACE FUNCTION public.auto_hide_content(
p_content_type VARCHAR,
p_content_id UUID,
p_triggered_word VARCHAR
)
RETURNS BOOLEAN AS $$
BEGIN
IF p_content_type = 'discussion' THEN
UPDATE public.discussions
SET
is_hidden = TRUE,
hidden_reason = 'Auto-moderated: contains banned word',
hidden_at = NOW(),
auto_moderated = TRUE,
triggered_word = p_triggered_word
WHERE id = p_content_id;
ELSIF p_content_type = 'reply' THEN
UPDATE public.discussion_replies
SET
is_hidden = TRUE,
hidden_reason = 'Auto-moderated: contains banned word',
hidden_at = NOW(),
auto_moderated = TRUE,
triggered_word = p_triggered_word
WHERE id = p_content_id;
ELSE
RETURN FALSE;
END IF;
INSERT INTO public.moderation_log (content_type, content_id, action, triggered_word)
VALUES (p_content_type, p_content_id, 'auto_hide', p_triggered_word);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Get banned words (for frontend filtering)
CREATE OR REPLACE FUNCTION public.get_banned_words(p_locale VARCHAR DEFAULT NULL)
RETURNS TABLE (
word VARCHAR,
is_regex BOOLEAN,
severity VARCHAR
) AS $$
BEGIN
RETURN QUERY
SELECT bw.word, bw.is_regex, bw.severity
FROM public.banned_words bw
WHERE bw.locale IS NULL OR bw.locale = p_locale;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Add banned word (admin only)
CREATE OR REPLACE FUNCTION public.admin_add_banned_word(
p_word VARCHAR,
p_locale VARCHAR DEFAULT NULL,
p_severity VARCHAR DEFAULT 'medium',
p_is_regex BOOLEAN DEFAULT FALSE
)
RETURNS UUID AS $$
DECLARE
new_id UUID;
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
INSERT INTO public.banned_words (word, locale, severity, is_regex, created_by)
VALUES (p_word, p_locale, p_severity, p_is_regex, auth.uid())
RETURNING id INTO new_id;
PERFORM public.log_admin_activity(
'add_banned_word',
'banned_word',
new_id::VARCHAR,
jsonb_build_object('word', p_word, 'locale', p_locale, 'severity', p_severity)
);
RETURN new_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function: Remove banned word (admin only)
CREATE OR REPLACE FUNCTION public.admin_remove_banned_word(p_word_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
IF NOT public.is_admin() THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
DELETE FROM public.banned_words WHERE id = p_word_id;
PERFORM public.log_admin_activity('remove_banned_word', 'banned_word', p_word_id::VARCHAR);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Update RLS policies for discussions (if table exists)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'discussions') THEN
DROP POLICY IF EXISTS "Users can view discussions" ON public.discussions;
CREATE POLICY "Users can view discussions" ON public.discussions
FOR SELECT USING (
(is_hidden = FALSE OR is_hidden IS NULL)
OR (is_hidden = TRUE AND author_id = auth.uid())
OR EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
END IF;
END $$;
-- Update RLS policies for discussion_replies (if table exists)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'discussion_replies') THEN
DROP POLICY IF EXISTS "Users can view replies" ON public.discussion_replies;
CREATE POLICY "Users can view replies" ON public.discussion_replies
FOR SELECT USING (
(is_hidden = FALSE OR is_hidden IS NULL)
OR (is_hidden = TRUE AND author_id = auth.uid())
OR EXISTS (SELECT 1 FROM public.admin_users WHERE user_id = auth.uid())
);
END IF;
END $$;
-- ============================================
-- GRANT PERMISSIONS
-- ============================================
GRANT SELECT ON public.admin_hidden_content TO authenticated;
GRANT EXECUTE ON FUNCTION public.is_admin TO authenticated;
GRANT EXECUTE ON FUNCTION public.get_admin_role TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_hide_content TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_unhide_content TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_delete_content TO authenticated;
GRANT EXECUTE ON FUNCTION public.auto_hide_content TO authenticated;
GRANT EXECUTE ON FUNCTION public.get_banned_words TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_add_banned_word TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_remove_banned_word TO authenticated;
GRANT EXECUTE ON FUNCTION public.log_admin_activity TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_ban_user TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_unban_user TO authenticated;
GRANT EXECUTE ON FUNCTION public.is_user_banned TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_review_report TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_toggle_user_reminders TO authenticated;
GRANT EXECUTE ON FUNCTION public.admin_update_setting TO authenticated;
-- ============================================
-- DONE! Now add yourself as super_admin:
--
-- INSERT INTO public.admin_users (user_id, role)
-- VALUES ('YOUR-USER-UUID-HERE', 'super_admin');
-- ============================================