130 lines
4.0 KiB
PL/PgSQL
130 lines
4.0 KiB
PL/PgSQL
-- User Suspensions Migration
|
|
-- Adds user_suspensions table for admin suspend/ban controls
|
|
|
|
-- 1. Create suspension type enum
|
|
DROP TYPE IF EXISTS public.suspension_type CASCADE;
|
|
CREATE TYPE public.suspension_type AS ENUM ('suspended', 'banned');
|
|
|
|
-- 2. Create user_suspensions table
|
|
CREATE TABLE IF NOT EXISTS public.user_suspensions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
|
|
suspended_by UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
|
|
suspension_type public.suspension_type NOT NULL DEFAULT 'suspended',
|
|
reason TEXT NOT NULL,
|
|
duration_days INTEGER NULL, -- NULL = permanent (ban)
|
|
expires_at TIMESTAMPTZ NULL, -- NULL = permanent
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
lifted_at TIMESTAMPTZ NULL,
|
|
lifted_by UUID NULL REFERENCES public.user_profiles(id) ON DELETE SET NULL,
|
|
lift_reason TEXT NULL,
|
|
email_sent BOOLEAN NOT NULL DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 3. Indexes
|
|
CREATE INDEX IF NOT EXISTS idx_user_suspensions_user_id ON public.user_suspensions(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_user_suspensions_active ON public.user_suspensions(user_id, is_active);
|
|
CREATE INDEX IF NOT EXISTS idx_user_suspensions_expires_at ON public.user_suspensions(expires_at) WHERE is_active = true;
|
|
|
|
-- 4. Add suspension_status column to user_profiles if not exists
|
|
ALTER TABLE public.user_profiles
|
|
ADD COLUMN IF NOT EXISTS suspension_status TEXT DEFAULT NULL,
|
|
ADD COLUMN IF NOT EXISTS suspension_reason TEXT DEFAULT NULL,
|
|
ADD COLUMN IF NOT EXISTS suspended_until TIMESTAMPTZ DEFAULT NULL;
|
|
|
|
-- 5. Helper function: check if user is currently suspended
|
|
CREATE OR REPLACE FUNCTION public.is_user_suspended(p_user_id UUID)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE sql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
AS $$
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM public.user_suspensions
|
|
WHERE user_id = p_user_id
|
|
AND is_active = true
|
|
AND (expires_at IS NULL OR expires_at > now())
|
|
)
|
|
$$;
|
|
|
|
-- 6. Helper function: get active suspension for user
|
|
CREATE OR REPLACE FUNCTION public.get_active_suspension(p_user_id UUID)
|
|
RETURNS TABLE(
|
|
suspension_id UUID,
|
|
suspension_type TEXT,
|
|
reason TEXT,
|
|
expires_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ
|
|
)
|
|
LANGUAGE sql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
AS $$
|
|
SELECT id, suspension_type::TEXT, reason, expires_at, created_at
|
|
FROM public.user_suspensions
|
|
WHERE user_id = p_user_id
|
|
AND is_active = true
|
|
AND (expires_at IS NULL OR expires_at > now())
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
$$;
|
|
|
|
-- 7. Function to auto-expire suspensions
|
|
CREATE OR REPLACE FUNCTION public.expire_suspensions()
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
AS $$
|
|
BEGIN
|
|
UPDATE public.user_suspensions
|
|
SET is_active = false, updated_at = now()
|
|
WHERE is_active = true
|
|
AND expires_at IS NOT NULL
|
|
AND expires_at <= now();
|
|
|
|
-- Sync user_profiles suspension_status
|
|
UPDATE public.user_profiles
|
|
SET suspension_status = NULL,
|
|
suspension_reason = NULL,
|
|
suspended_until = NULL
|
|
WHERE id IN (
|
|
SELECT DISTINCT user_id FROM public.user_suspensions
|
|
WHERE is_active = false
|
|
AND expires_at IS NOT NULL
|
|
AND expires_at <= now()
|
|
)
|
|
AND NOT public.is_user_suspended(id);
|
|
END;
|
|
$$;
|
|
|
|
-- 8. Enable RLS
|
|
ALTER TABLE public.user_suspensions ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- 9. RLS Policies
|
|
DROP POLICY IF EXISTS "admins_manage_suspensions" ON public.user_suspensions;
|
|
CREATE POLICY "admins_manage_suspensions"
|
|
ON public.user_suspensions
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM public.user_profiles
|
|
WHERE id = auth.uid() AND role IN ('admin', 'moderator')
|
|
)
|
|
)
|
|
WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM public.user_profiles
|
|
WHERE id = auth.uid() AND role IN ('admin', 'moderator')
|
|
)
|
|
);
|
|
|
|
DROP POLICY IF EXISTS "users_view_own_suspension" ON public.user_suspensions;
|
|
CREATE POLICY "users_view_own_suspension"
|
|
ON public.user_suspensions
|
|
FOR SELECT
|
|
TO authenticated
|
|
USING (user_id = auth.uid());
|