feat: Implement comprehensive ban enforcement

This commit is contained in:
gpt-engineer-app[bot]
2025-10-30 01:47:51 +00:00
parent f95eaf9eb7
commit fe76c8c572
8 changed files with 452 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
import { createClient, SupabaseClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
import { edgeLogger } from "./logger.ts";
export async function checkUserBanned(
userId: string,
supabase: SupabaseClient
): Promise<{ banned: boolean; error?: string }> {
try {
const { data: profile, error } = await supabase
.from('profiles')
.select('banned')
.eq('user_id', userId)
.single();
if (error) {
edgeLogger.error('Ban check failed', { userId, error: error.message });
return { banned: false, error: 'Unable to verify account status' };
}
if (!profile) {
return { banned: false, error: 'Profile not found' };
}
return { banned: profile.banned };
} catch (error) {
edgeLogger.error('Ban check exception', { userId, error });
return { banned: false, error: 'Internal error checking account status' };
}
}
export function createBannedResponse(requestId: string, corsHeaders: Record<string, string>) {
return new Response(
JSON.stringify({
error: 'Account suspended',
message: 'Your account has been suspended. Contact support for assistance.',
requestId
}),
{
status: 403,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': requestId
}
}
);
}

View File

@@ -116,6 +116,60 @@ serve(async (req) => {
edgeLogger.info('Authentication successful', { action: 'approval_auth_success', userId: user.id });
// Check if user is banned
const { data: profile, error: profileError } = await supabaseAuth
.from('profiles')
.select('banned')
.eq('user_id', user.id)
.single();
if (profileError || !profile) {
edgeLogger.error('Profile check failed', {
action: 'approval_profile_check',
error: profileError?.message,
requestId: tracking.requestId
});
const duration = endRequest(tracking);
return new Response(
JSON.stringify({
error: 'Unable to verify user profile',
requestId: tracking.requestId
}),
{
status: 403,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
);
}
if (profile.banned) {
edgeLogger.warn('Banned user attempted approval', {
action: 'approval_banned_user',
userId: user.id,
requestId: tracking.requestId
});
const duration = endRequest(tracking);
return new Response(
JSON.stringify({
error: 'Account suspended',
message: 'Your account has been suspended. Contact support for assistance.',
requestId: tracking.requestId
}),
{
status: 403,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
}
}
);
}
// SECURITY NOTE: Service role key used later in this function
// Reason: Need to bypass RLS to write approved changes to entity tables
// (parks, rides, companies, ride_models) which have RLS policies

View File

@@ -0,0 +1,167 @@
-- Phase 1 & 4: Comprehensive Ban Enforcement (Corrected)
-- Create security definer function to check if user is banned
CREATE OR REPLACE FUNCTION public.is_user_banned(_user_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT COALESCE(
(SELECT banned FROM public.profiles WHERE user_id = _user_id),
false
)
$$;
-- Update RLS policies for content_submissions
DROP POLICY IF EXISTS "Users can create submissions" ON content_submissions;
CREATE POLICY "Users can create submissions"
ON content_submissions
FOR INSERT
TO authenticated
WITH CHECK (
(auth.uid() = user_id)
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for park_submissions
DROP POLICY IF EXISTS "Users can insert their own park submissions" ON park_submissions;
CREATE POLICY "Users can insert their own park submissions"
ON park_submissions
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (SELECT 1 FROM content_submissions cs WHERE cs.id = park_submissions.submission_id AND cs.user_id = auth.uid())
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for ride_submissions
DROP POLICY IF EXISTS "Users can insert their own ride submissions" ON ride_submissions;
CREATE POLICY "Users can insert their own ride submissions"
ON ride_submissions
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (SELECT 1 FROM content_submissions cs WHERE cs.id = ride_submissions.submission_id AND cs.user_id = auth.uid())
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for company_submissions
DROP POLICY IF EXISTS "Users can insert their own company submissions" ON company_submissions;
CREATE POLICY "Users can insert their own company submissions"
ON company_submissions
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (SELECT 1 FROM content_submissions cs WHERE cs.id = company_submissions.submission_id AND cs.user_id = auth.uid())
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for ride_model_submissions
DROP POLICY IF EXISTS "Users can insert their own ride model submissions" ON ride_model_submissions;
CREATE POLICY "Users can insert their own ride model submissions"
ON ride_model_submissions
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (SELECT 1 FROM content_submissions cs WHERE cs.id = ride_model_submissions.submission_id AND cs.user_id = auth.uid())
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for photo_submissions
DROP POLICY IF EXISTS "Users can insert their own photo submissions" ON photo_submissions;
CREATE POLICY "Users can insert their own photo submissions"
ON photo_submissions
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (SELECT 1 FROM content_submissions cs WHERE cs.id = photo_submissions.submission_id AND cs.user_id = auth.uid())
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for submission_items
DROP POLICY IF EXISTS "Users can insert their own submission items" ON submission_items;
CREATE POLICY "Users can insert their own submission items"
ON submission_items
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (SELECT 1 FROM content_submissions cs WHERE cs.id = submission_items.submission_id AND cs.user_id = auth.uid())
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for reviews (using user_id)
DROP POLICY IF EXISTS "Users can create reviews" ON reviews;
CREATE POLICY "Users can create reviews"
ON reviews
FOR INSERT
TO authenticated
WITH CHECK (
user_id = auth.uid()
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for photos (using submitted_by)
DROP POLICY IF EXISTS "Users can insert their own photos" ON photos;
CREATE POLICY "Users can insert their own photos"
ON photos
FOR INSERT
TO authenticated
WITH CHECK (
submitted_by = auth.uid()
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for user_blocks
DROP POLICY IF EXISTS "Users can block other users" ON user_blocks;
CREATE POLICY "Users can block other users"
ON user_blocks
FOR INSERT
TO authenticated
WITH CHECK (
blocker_id = auth.uid()
AND NOT is_user_banned(auth.uid())
);
-- Update RLS policies for contact_submissions
DROP POLICY IF EXISTS "Anyone can submit contact form" ON contact_submissions;
CREATE POLICY "Anyone can submit contact form"
ON contact_submissions
FOR INSERT
TO authenticated
WITH CHECK (
(user_id = auth.uid() OR user_id IS NULL)
AND NOT is_user_banned(auth.uid())
);
-- Phase 4: Create trigger to invalidate sessions when user is banned
CREATE OR REPLACE FUNCTION public.invalidate_banned_user_sessions()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- If user was just banned (banned changed from false to true)
IF NEW.banned = true AND OLD.banned = false THEN
-- Notify application layer about the ban
PERFORM pg_notify(
'user_banned',
json_build_object(
'user_id', NEW.user_id,
'username', NEW.username,
'banned_at', NOW()
)::text
);
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS enforce_ban_on_profile_update ON public.profiles;
CREATE TRIGGER enforce_ban_on_profile_update
AFTER UPDATE ON public.profiles
FOR EACH ROW
WHEN (NEW.banned IS DISTINCT FROM OLD.banned)
EXECUTE FUNCTION invalidate_banned_user_sessions();