mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:11:12 -05:00
feat: Implement comprehensive ban enforcement
This commit is contained in:
@@ -110,6 +110,19 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
|||||||
const currentAal = await getSessionAal(session);
|
const currentAal = await getSessionAal(session);
|
||||||
setAal(currentAal);
|
setAal(currentAal);
|
||||||
authLog('[Auth] Current AAL:', currentAal);
|
authLog('[Auth] Current AAL:', currentAal);
|
||||||
|
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', session.user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
authWarn('[Auth] Banned user detected, signing out');
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setAal(null);
|
setAal(null);
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/hooks/useBanCheck.ts
Normal file
82
src/hooks/useBanCheck.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { toast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
export function useBanCheck() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isBanned, setIsBanned] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
setIsBanned(false);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkBan = async () => {
|
||||||
|
try {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
setIsBanned(true);
|
||||||
|
toast({
|
||||||
|
title: 'Account Suspended',
|
||||||
|
description: 'Your account has been suspended. Contact support for assistance.',
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: Infinity // Don't auto-dismiss
|
||||||
|
});
|
||||||
|
// Sign out banned user
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ban check error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkBan();
|
||||||
|
|
||||||
|
// Subscribe to profile changes (real-time ban detection)
|
||||||
|
const channel = supabase
|
||||||
|
.channel('ban-check')
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: 'UPDATE',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'profiles',
|
||||||
|
filter: `user_id=eq.${user.id}`
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
if (payload.new && (payload.new as { banned: boolean }).banned) {
|
||||||
|
setIsBanned(true);
|
||||||
|
toast({
|
||||||
|
title: 'Account Suspended',
|
||||||
|
description: 'Your account has been suspended. Contact support for assistance.',
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: Infinity
|
||||||
|
});
|
||||||
|
supabase.auth.signOut();
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel);
|
||||||
|
};
|
||||||
|
}, [user, navigate]);
|
||||||
|
|
||||||
|
return { isBanned, loading };
|
||||||
|
}
|
||||||
@@ -4099,6 +4099,7 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
is_moderator: { Args: { _user_id: string }; Returns: boolean }
|
is_moderator: { Args: { _user_id: string }; Returns: boolean }
|
||||||
is_superuser: { Args: { _user_id: string }; Returns: boolean }
|
is_superuser: { Args: { _user_id: string }; Returns: boolean }
|
||||||
|
is_user_banned: { Args: { _user_id: string }; Returns: boolean }
|
||||||
log_admin_action: {
|
log_admin_action: {
|
||||||
Args: {
|
Args: {
|
||||||
_action: string
|
_action: string
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ export async function submitCompanyCreation(
|
|||||||
companyType: 'manufacturer' | 'designer' | 'operator' | 'property_owner',
|
companyType: 'manufacturer' | 'designer' | 'operator' | 'property_owner',
|
||||||
userId: string
|
userId: string
|
||||||
) {
|
) {
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
// Upload any pending local images first
|
// Upload any pending local images first
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
@@ -78,6 +89,17 @@ export async function submitCompanyUpdate(
|
|||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
) {
|
) {
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch existing company data (all fields for original_data)
|
// Fetch existing company data (all fields for original_data)
|
||||||
const { data: existingCompany, error: fetchError } = await supabase
|
const { data: existingCompany, error: fetchError } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
|
|||||||
@@ -177,6 +177,17 @@ export async function submitParkCreation(
|
|||||||
data: ParkFormData,
|
data: ParkFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
// Upload any pending local images first
|
// Upload any pending local images first
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
@@ -257,6 +268,17 @@ export async function submitParkUpdate(
|
|||||||
data: ParkFormData,
|
data: ParkFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch existing park data first
|
// Fetch existing park data first
|
||||||
const { data: existingPark, error: fetchError } = await supabase
|
const { data: existingPark, error: fetchError } = await supabase
|
||||||
.from('parks')
|
.from('parks')
|
||||||
@@ -349,6 +371,17 @@ export async function submitRideCreation(
|
|||||||
data: RideFormData,
|
data: RideFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
// Upload any pending local images first
|
// Upload any pending local images first
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
@@ -429,6 +462,17 @@ export async function submitRideUpdate(
|
|||||||
data: RideFormData,
|
data: RideFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch existing ride data first
|
// Fetch existing ride data first
|
||||||
const { data: existingRide, error: fetchError } = await supabase
|
const { data: existingRide, error: fetchError } = await supabase
|
||||||
.from('rides')
|
.from('rides')
|
||||||
@@ -518,6 +562,17 @@ export async function submitRideModelCreation(
|
|||||||
data: RideModelFormData,
|
data: RideModelFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
// Upload any pending local images first
|
// Upload any pending local images first
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
@@ -584,6 +639,17 @@ export async function submitRideModelUpdate(
|
|||||||
data: RideModelFormData,
|
data: RideModelFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Check if user is banned
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch existing ride model
|
// Fetch existing ride model
|
||||||
const { data: existingModel, error: fetchError } = await supabase
|
const { data: existingModel, error: fetchError } = await supabase
|
||||||
.from('ride_models')
|
.from('ride_models')
|
||||||
|
|||||||
47
supabase/functions/_shared/banCheck.ts
Normal file
47
supabase/functions/_shared/banCheck.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -116,6 +116,60 @@ serve(async (req) => {
|
|||||||
|
|
||||||
edgeLogger.info('Authentication successful', { action: 'approval_auth_success', userId: user.id });
|
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
|
// SECURITY NOTE: Service role key used later in this function
|
||||||
// Reason: Need to bypass RLS to write approved changes to entity tables
|
// Reason: Need to bypass RLS to write approved changes to entity tables
|
||||||
// (parks, rides, companies, ride_models) which have RLS policies
|
// (parks, rides, companies, ride_models) which have RLS policies
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user