Add ban reason to profiles

This commit is contained in:
gpt-engineer-app[bot]
2025-10-30 02:51:16 +00:00
parent e0d1a66fb2
commit e5de404e59
8 changed files with 146 additions and 10 deletions

View File

@@ -82,6 +82,31 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
const { data, error } = await supabase.auth.signInWithPassword(signInOptions); const { data, error } = await supabase.auth.signInWithPassword(signInOptions);
if (error) throw error; 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) // Check if MFA is required (user exists but no session)
if (data.user && !data.session) { if (data.user && !data.session) {
const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified'); const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified');

View File

@@ -8,6 +8,7 @@ export function useBanCheck() {
const { user } = useAuth(); const { user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [isBanned, setIsBanned] = useState(false); const [isBanned, setIsBanned] = useState(false);
const [banReason, setBanReason] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -21,15 +22,21 @@ export function useBanCheck() {
try { try {
const { data: profile } = await supabase const { data: profile } = await supabase
.from('profiles') .from('profiles')
.select('banned') .select('banned, ban_reason')
.eq('user_id', user.id) .eq('user_id', user.id)
.single(); .single();
if (profile?.banned) { if (profile?.banned) {
setIsBanned(true); setIsBanned(true);
setBanReason(profile.ban_reason || null);
const reason = profile.ban_reason
? `Reason: ${profile.ban_reason}`
: 'Contact support for assistance.';
toast({ toast({
title: 'Account Suspended', title: 'Account Suspended',
description: 'Your account has been suspended. Contact support for assistance.', description: `Your account has been suspended. ${reason}`,
variant: 'destructive', variant: 'destructive',
duration: Infinity // Don't auto-dismiss duration: Infinity // Don't auto-dismiss
}); });
@@ -58,14 +65,20 @@ export function useBanCheck() {
filter: `user_id=eq.${user.id}` filter: `user_id=eq.${user.id}`
}, },
(payload) => { (payload) => {
const newProfile = payload.new as { banned: boolean }; const newProfile = payload.new as { banned: boolean; ban_reason: string | null };
// Handle BAN event // Handle BAN event
if (newProfile.banned && !isBanned) { if (newProfile.banned && !isBanned) {
setIsBanned(true); setIsBanned(true);
setBanReason(newProfile.ban_reason || null);
const reason = newProfile.ban_reason
? `Reason: ${newProfile.ban_reason}`
: 'Contact support for assistance.';
toast({ toast({
title: 'Account Suspended', title: 'Account Suspended',
description: 'Your account has been suspended. Contact support for assistance.', description: `Your account has been suspended. ${reason}`,
variant: 'destructive', variant: 'destructive',
duration: Infinity duration: Infinity
}); });
@@ -76,6 +89,7 @@ export function useBanCheck() {
// Handle UNBAN event // Handle UNBAN event
if (!newProfile.banned && isBanned) { if (!newProfile.banned && isBanned) {
setIsBanned(false); setIsBanned(false);
setBanReason(null);
toast({ toast({
title: 'Account Restored', title: 'Account Restored',
description: 'Your account has been unbanned. You can now use the application normally.', description: 'Your account has been unbanned. You can now use the application normally.',
@@ -92,5 +106,5 @@ export function useBanCheck() {
}; };
}, [user, navigate]); }, [user, navigate]);
return { isBanned, loading }; return { isBanned, loading, banReason };
} }

View File

@@ -1952,6 +1952,7 @@ export type Database = {
Row: { Row: {
avatar_image_id: string | null avatar_image_id: string | null
avatar_url: string | null avatar_url: string | null
ban_reason: string | null
banned: boolean banned: boolean
bio: string | null bio: string | null
coaster_count: number | null coaster_count: number | null
@@ -1983,6 +1984,7 @@ export type Database = {
Insert: { Insert: {
avatar_image_id?: string | null avatar_image_id?: string | null
avatar_url?: string | null avatar_url?: string | null
ban_reason?: string | null
banned?: boolean banned?: boolean
bio?: string | null bio?: string | null
coaster_count?: number | null coaster_count?: number | null
@@ -2014,6 +2016,7 @@ export type Database = {
Update: { Update: {
avatar_image_id?: string | null avatar_image_id?: string | null
avatar_url?: string | null avatar_url?: string | null
ban_reason?: string | null
banned?: boolean banned?: boolean
bio?: string | null bio?: string | null
coaster_count?: number | null coaster_count?: number | null

View File

@@ -104,6 +104,31 @@ export default function Auth() {
if (error) throw error; 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) // Check if MFA is required (user exists but no session)
if (data.user && !data.session) { if (data.user && !data.session) {
const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified'); const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified');

View File

@@ -51,6 +51,31 @@ export default function AuthCallback() {
const user = session.user; 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) // Check if this is a new OAuth user (created within last minute)
const createdAt = new Date(user.created_at); const createdAt = new Date(user.created_at);
const now = new Date(); const now = new Date();

View File

@@ -4,11 +4,11 @@ import { edgeLogger } from "./logger.ts";
export async function checkUserBanned( export async function checkUserBanned(
userId: string, userId: string,
supabase: SupabaseClient supabase: SupabaseClient
): Promise<{ banned: boolean; error?: string }> { ): Promise<{ banned: boolean; ban_reason?: string; error?: string }> {
try { try {
const { data: profile, error } = await supabase const { data: profile, error } = await supabase
.from('profiles') .from('profiles')
.select('banned') .select('banned, ban_reason')
.eq('user_id', userId) .eq('user_id', userId)
.single(); .single();
@@ -21,18 +21,26 @@ export async function checkUserBanned(
return { banned: false, error: 'Profile not found' }; return { banned: false, error: 'Profile not found' };
} }
return { banned: profile.banned }; return {
banned: profile.banned,
ban_reason: profile.ban_reason || undefined
};
} catch (error) { } catch (error) {
edgeLogger.error('Ban check exception', { userId, error }); edgeLogger.error('Ban check exception', { userId, error });
return { banned: false, error: 'Internal error checking account status' }; return { banned: false, error: 'Internal error checking account status' };
} }
} }
export function createBannedResponse(requestId: string, corsHeaders: Record<string, string>) { export function createBannedResponse(requestId: string, corsHeaders: Record<string, string>, 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( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Account suspended', error: 'Account suspended',
message: 'Your account has been suspended. Contact support for assistance.', message,
ban_reason,
requestId requestId
}), }),
{ {

View File

@@ -105,6 +105,36 @@ Deno.serve(async (req) => {
console.log('[OAuth Profile] Processing profile for user:', user.id); 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; const provider = user.app_metadata?.provider;
// For Discord, data is in identities[0].identity_data, not user_metadata // For Discord, data is in identities[0].identity_data, not user_metadata

View File

@@ -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.';