From e5de404e59bc5978e2582fd01b26fa9af5e199eb Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 02:51:16 +0000 Subject: [PATCH] Add ban reason to profiles --- src/components/auth/AuthModal.tsx | 25 ++++++++++++++++ src/hooks/useBanCheck.ts | 24 +++++++++++---- src/integrations/supabase/types.ts | 3 ++ src/pages/Auth.tsx | 25 ++++++++++++++++ src/pages/AuthCallback.tsx | 25 ++++++++++++++++ supabase/functions/_shared/banCheck.ts | 18 +++++++---- .../functions/process-oauth-profile/index.ts | 30 +++++++++++++++++++ ...0_5ffe10ae-db09-4939-b81a-3b0b7cc07273.sql | 6 ++++ 8 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 supabase/migrations/20251030025000_5ffe10ae-db09-4939-b81a-3b0b7cc07273.sql diff --git a/src/components/auth/AuthModal.tsx b/src/components/auth/AuthModal.tsx index 3551c475..f47af9f1 100644 --- a/src/components/auth/AuthModal.tsx +++ b/src/components/auth/AuthModal.tsx @@ -82,6 +82,31 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod const { data, error } = await supabase.auth.signInWithPassword(signInOptions); if (error) throw error; + // CRITICAL: Check ban status immediately after successful authentication + const { data: profile } = await supabase + .from('profiles') + .select('banned, ban_reason') + .eq('user_id', data.user.id) + .single(); + + if (profile?.banned) { + // Sign out immediately + await supabase.auth.signOut(); + + const reason = profile.ban_reason + ? `Reason: ${profile.ban_reason}` + : 'Contact support for assistance.'; + + toast({ + variant: "destructive", + title: "Account Suspended", + description: `Your account has been suspended. ${reason}`, + duration: 10000 + }); + setLoading(false); + return; // Stop authentication flow + } + // Check if MFA is required (user exists but no session) if (data.user && !data.session) { const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified'); diff --git a/src/hooks/useBanCheck.ts b/src/hooks/useBanCheck.ts index 81457437..1ce81dc4 100644 --- a/src/hooks/useBanCheck.ts +++ b/src/hooks/useBanCheck.ts @@ -8,6 +8,7 @@ export function useBanCheck() { const { user } = useAuth(); const navigate = useNavigate(); const [isBanned, setIsBanned] = useState(false); + const [banReason, setBanReason] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -21,15 +22,21 @@ export function useBanCheck() { try { const { data: profile } = await supabase .from('profiles') - .select('banned') + .select('banned, ban_reason') .eq('user_id', user.id) .single(); if (profile?.banned) { setIsBanned(true); + setBanReason(profile.ban_reason || null); + + const reason = profile.ban_reason + ? `Reason: ${profile.ban_reason}` + : 'Contact support for assistance.'; + toast({ title: 'Account Suspended', - description: 'Your account has been suspended. Contact support for assistance.', + description: `Your account has been suspended. ${reason}`, variant: 'destructive', duration: Infinity // Don't auto-dismiss }); @@ -58,14 +65,20 @@ export function useBanCheck() { filter: `user_id=eq.${user.id}` }, (payload) => { - const newProfile = payload.new as { banned: boolean }; + const newProfile = payload.new as { banned: boolean; ban_reason: string | null }; // Handle BAN event if (newProfile.banned && !isBanned) { setIsBanned(true); + setBanReason(newProfile.ban_reason || null); + + const reason = newProfile.ban_reason + ? `Reason: ${newProfile.ban_reason}` + : 'Contact support for assistance.'; + toast({ title: 'Account Suspended', - description: 'Your account has been suspended. Contact support for assistance.', + description: `Your account has been suspended. ${reason}`, variant: 'destructive', duration: Infinity }); @@ -76,6 +89,7 @@ export function useBanCheck() { // Handle UNBAN event if (!newProfile.banned && isBanned) { setIsBanned(false); + setBanReason(null); toast({ title: 'Account Restored', description: 'Your account has been unbanned. You can now use the application normally.', @@ -92,5 +106,5 @@ export function useBanCheck() { }; }, [user, navigate]); - return { isBanned, loading }; + return { isBanned, loading, banReason }; } diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 20a9cc88..d873a886 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -1952,6 +1952,7 @@ export type Database = { Row: { avatar_image_id: string | null avatar_url: string | null + ban_reason: string | null banned: boolean bio: string | null coaster_count: number | null @@ -1983,6 +1984,7 @@ export type Database = { Insert: { avatar_image_id?: string | null avatar_url?: string | null + ban_reason?: string | null banned?: boolean bio?: string | null coaster_count?: number | null @@ -2014,6 +2016,7 @@ export type Database = { Update: { avatar_image_id?: string | null avatar_url?: string | null + ban_reason?: string | null banned?: boolean bio?: string | null coaster_count?: number | null diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx index 7d3af131..ce460ae8 100644 --- a/src/pages/Auth.tsx +++ b/src/pages/Auth.tsx @@ -104,6 +104,31 @@ export default function Auth() { if (error) throw error; + // CRITICAL: Check ban status immediately after successful authentication + const { data: profile } = await supabase + .from('profiles') + .select('banned, ban_reason') + .eq('user_id', data.user.id) + .single(); + + if (profile?.banned) { + // Sign out immediately + await supabase.auth.signOut(); + + const reason = profile.ban_reason + ? `Reason: ${profile.ban_reason}` + : 'Contact support for assistance.'; + + toast({ + variant: "destructive", + title: "Account Suspended", + description: `Your account has been suspended. ${reason}`, + duration: 10000 + }); + setLoading(false); + return; // Stop authentication flow + } + // Check if MFA is required (user exists but no session) if (data.user && !data.session) { const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified'); diff --git a/src/pages/AuthCallback.tsx b/src/pages/AuthCallback.tsx index 123e768c..9e82d090 100644 --- a/src/pages/AuthCallback.tsx +++ b/src/pages/AuthCallback.tsx @@ -51,6 +51,31 @@ export default function AuthCallback() { const user = session.user; + // CRITICAL: Check ban status immediately after getting session + const { data: banProfile } = await supabase + .from('profiles') + .select('banned, ban_reason') + .eq('user_id', user.id) + .single(); + + if (banProfile?.banned) { + await supabase.auth.signOut(); + + const reason = banProfile.ban_reason + ? `Reason: ${banProfile.ban_reason}` + : 'Contact support for assistance.'; + + toast({ + variant: 'destructive', + title: 'Account Suspended', + description: `Your account has been suspended. ${reason}`, + duration: 10000 + }); + + navigate('/auth'); + return; // Stop OAuth processing + } + // Check if this is a new OAuth user (created within last minute) const createdAt = new Date(user.created_at); const now = new Date(); diff --git a/supabase/functions/_shared/banCheck.ts b/supabase/functions/_shared/banCheck.ts index b066f981..94726365 100644 --- a/supabase/functions/_shared/banCheck.ts +++ b/supabase/functions/_shared/banCheck.ts @@ -4,11 +4,11 @@ import { edgeLogger } from "./logger.ts"; export async function checkUserBanned( userId: string, supabase: SupabaseClient -): Promise<{ banned: boolean; error?: string }> { +): Promise<{ banned: boolean; ban_reason?: string; error?: string }> { try { const { data: profile, error } = await supabase .from('profiles') - .select('banned') + .select('banned, ban_reason') .eq('user_id', userId) .single(); @@ -21,18 +21,26 @@ export async function checkUserBanned( return { banned: false, error: 'Profile not found' }; } - return { banned: profile.banned }; + return { + banned: profile.banned, + ban_reason: profile.ban_reason || undefined + }; } 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) { +export function createBannedResponse(requestId: string, corsHeaders: Record, ban_reason?: string) { + const message = ban_reason + ? `Your account has been suspended. Reason: ${ban_reason}` + : 'Your account has been suspended. Contact support for assistance.'; + return new Response( JSON.stringify({ error: 'Account suspended', - message: 'Your account has been suspended. Contact support for assistance.', + message, + ban_reason, requestId }), { diff --git a/supabase/functions/process-oauth-profile/index.ts b/supabase/functions/process-oauth-profile/index.ts index dc96d2c1..2845015f 100644 --- a/supabase/functions/process-oauth-profile/index.ts +++ b/supabase/functions/process-oauth-profile/index.ts @@ -105,6 +105,36 @@ Deno.serve(async (req) => { console.log('[OAuth Profile] Processing profile for user:', user.id); + // CRITICAL: Check ban status immediately + const { data: banProfile } = await supabase + .from('profiles') + .select('banned, ban_reason') + .eq('user_id', user.id) + .single(); + + if (banProfile?.banned) { + const duration = endRequest(tracking); + const message = banProfile.ban_reason + ? `Your account has been suspended. Reason: ${banProfile.ban_reason}` + : 'Your account has been suspended. Contact support for assistance.'; + + console.log('[OAuth Profile] User is banned, rejecting authentication', { + requestId: tracking.requestId, + duration, + hasBanReason: !!banProfile.ban_reason + }); + + return new Response(JSON.stringify({ + error: 'Account suspended', + message, + ban_reason: banProfile.ban_reason, + requestId: tracking.requestId + }), { + status: 403, + headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, + }); + } + const provider = user.app_metadata?.provider; // For Discord, data is in identities[0].identity_data, not user_metadata diff --git a/supabase/migrations/20251030025000_5ffe10ae-db09-4939-b81a-3b0b7cc07273.sql b/supabase/migrations/20251030025000_5ffe10ae-db09-4939-b81a-3b0b7cc07273.sql new file mode 100644 index 00000000..73467b59 --- /dev/null +++ b/supabase/migrations/20251030025000_5ffe10ae-db09-4939-b81a-3b0b7cc07273.sql @@ -0,0 +1,6 @@ +-- Add ban_reason column to profiles table +ALTER TABLE public.profiles +ADD COLUMN IF NOT EXISTS ban_reason text; + +COMMENT ON COLUMN public.profiles.ban_reason IS +'Explanation for why user account was suspended. Only visible to moderators and the banned user during login attempts.';