mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
Add ban reason to profiles
This commit is contained in:
@@ -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');
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.';
|
||||||
Reference in New Issue
Block a user