Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,357 changes: 807 additions & 550 deletions app/admin/reports/page.tsx

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions app/admin/users/[userId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { User, Mail, Calendar, Package, Star, Activity, Clock, AlertTriangle, Ban, CheckCircle, Shield, ArrowLeft } from 'lucide-react';
import { supabase } from '../../../lib/supabaseClient';
import { AdminService } from '../../../lib/database/AdminService';
import { useAuth } from '../../../context/AuthContext';
import Image from 'next/image';
import AdminLayout from '../../../../components/admin/AdminLayout';
Expand Down Expand Up @@ -148,24 +149,29 @@ const AdminUserProfilePage = () => {

const handleBanUser = async () => {
if (!currentUser?.id || !profile) return;

const action = profile.is_banned ? 'unban' : 'ban';
if (!confirm(`Are you sure you want to ${action} this user?`)) {
return;
}

setActionLoading(true);
try {
const { error } = await supabase
.from('users')
.update({ is_banned: !profile.is_banned })
.eq('id', userId as string);

if (error) {
throw new Error(error.message);
let result;

if (profile.is_banned) {
// Unban the user
result = await AdminService.unbanUser(userId as string, currentUser.id);
} else {
// Ban the user
result = await AdminService.banUser(userId as string, currentUser.id);
}

setProfile({ ...profile, is_banned: !profile.is_banned });
if (result.success) {
setProfile({ ...profile, is_banned: !profile.is_banned });
} else {
alert(result.error || `Failed to ${action} user`);
}
} catch (error) {
console.error(`Error ${action}ning user:`, error);
alert(`Failed to ${action} user`);
Expand Down
81 changes: 58 additions & 23 deletions app/admin/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react';
import { AdminService } from '../../lib/database/AdminService';
import { supabase } from '../../lib/supabaseClient';
import { User, Shield, Search, Ban, CheckCircle2, ExternalLink } from 'lucide-react';
import { User, Shield, Search, Ban, CheckCircle2, ExternalLink, Clock } from 'lucide-react';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Image from 'next/image';
Expand All @@ -18,6 +18,8 @@ interface UserData {
last_sign_in_at?: string;
is_admin?: boolean;
is_banned?: boolean;
is_suspended?: boolean;
suspension_until?: string | null;
listing_count?: number;
review_count?: number;
average_rating?: number;
Expand All @@ -27,16 +29,28 @@ const AdminUsersPage = () => {
const [users, setUsers] = useState<UserData[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'banned' | 'admin'>('all');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'banned' | 'suspended' | 'admin'>('all');
const [currentAdminId, setCurrentAdminId] = useState<string | null>(null);

useEffect(() => {
fetchCurrentUser();
fetchUsers();
}, []);

const fetchCurrentUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (user) {
setCurrentAdminId(user.id);
}
};

const fetchUsers = async () => {
try {
setLoading(true);


// Clear any expired suspensions before loading (fire-and-forget)
fetch('/api/admin/lift-expired-suspensions', { method: 'POST' }).catch(() => {});

// First, fetch users with error handling for missing columns
const { data: usersData, error: usersError } = await supabase
.from('users')
Expand Down Expand Up @@ -87,6 +101,8 @@ const AdminUsersPage = () => {
// Handle missing columns gracefully
is_admin: user.is_admin ?? false,
is_banned: user.is_banned ?? false,
is_suspended: user.is_suspended ?? false,
suspension_until: user.suspension_until ?? null,
display_name: user.display_name ?? null,
profile_image_url: user.profile_image_url ?? null,
last_sign_in_at: user.last_sign_in_at ?? null,
Expand All @@ -100,6 +116,8 @@ const AdminUsersPage = () => {
...user,
is_admin: user.is_admin ?? false,
is_banned: user.is_banned ?? false,
is_suspended: user.is_suspended ?? false,
suspension_until: user.suspension_until ?? null,
display_name: user.display_name ?? null,
profile_image_url: user.profile_image_url ?? null,
last_sign_in_at: user.last_sign_in_at ?? null,
Expand All @@ -126,32 +144,33 @@ const AdminUsersPage = () => {

const handleBanUser = async (userId: string, currentBanStatus: boolean) => {
const action = currentBanStatus ? 'unban' : 'ban';

if (!confirm(`Are you sure you want to ${action} this user?`)) {
return;
}

if (!currentAdminId) {
toast.error('Admin session not found. Please refresh the page.');
return;
}

try {
// Try to update the is_banned column, but handle gracefully if it doesn't exist
const { error } = await supabase
.from('users')
.update({ is_banned: !currentBanStatus })
.eq('id', userId);
let result;

if (error) {
// If the column doesn't exist, we could add it or show a different message
console.error(`Error ${action}ning user:`, error);

if (error.message?.includes('column') && error.message?.includes('does not exist')) {
toast.error(`Ban functionality requires database migration. Column 'is_banned' not found.`);
} else {
toast.error(`Failed to ${action} user: ${error.message}`);
}
return;
if (currentBanStatus) {
// Unban the user
result = await AdminService.unbanUser(userId, currentAdminId);
} else {
// Ban the user
result = await AdminService.banUser(userId, currentAdminId);
}

toast.success(`User ${action}ned successfully`);
await fetchUsers();
if (result.success) {
toast.success(`User ${action}ned successfully`);
await fetchUsers();
} else {
toast.error(result.error || `Failed to ${action} user`);
}
} catch (error) {
console.error(`Error ${action}ning user:`, error);
toast.error(`Error ${action}ning user`);
Expand Down Expand Up @@ -196,10 +215,11 @@ const AdminUsersPage = () => {
user.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
(user.display_name && user.display_name.toLowerCase().includes(searchTerm.toLowerCase()));

const matchesStatus =
const matchesStatus =
statusFilter === 'all' ||
(statusFilter === 'active' && !user.is_banned && !user.is_admin) ||
(statusFilter === 'active' && !user.is_banned && !user.is_suspended && !user.is_admin) ||
(statusFilter === 'banned' && user.is_banned === true) ||
(statusFilter === 'suspended' && user.is_suspended === true) ||
(statusFilter === 'admin' && user.is_admin === true);

return matchesSearch && matchesStatus;
Expand All @@ -222,6 +242,20 @@ const AdminUsersPage = () => {
</span>
);
}
if (user.is_suspended === true && user.suspension_until) {
const msLeft = new Date(user.suspension_until).getTime() - Date.now();
const daysLeft = Math.ceil(msLeft / (1000 * 60 * 60 * 24));
const label = daysLeft > 0 ? `${daysLeft}d left` : 'Expiring';
return (
<div className="flex flex-col gap-0.5">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
<Clock size={12} className="mr-1" />
Suspended
</span>
<span className="text-[10px] text-orange-600 pl-1">{label}</span>
</div>
);
}
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle2 size={12} className="mr-1" />
Expand Down Expand Up @@ -274,6 +308,7 @@ const AdminUsersPage = () => {
>
<option value="all">All Users</option>
<option value="active">Active Users</option>
<option value="suspended">Suspended Users</option>
<option value="banned">Banned Users</option>
<option value="admin">Administrators</option>
</select>
Expand Down
104 changes: 104 additions & 0 deletions app/api/admin/approve-listing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!supabaseUrl || !supabaseServiceRole) {
throw new Error('Missing Supabase environment variables');
}

// Server-side admin client with service role
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceRole, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});

// UUID validation regex
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function isValidUUID(uuid: string): boolean {
return UUID_REGEX.test(uuid);
}

export async function POST(request: NextRequest) {
try {
// Parse and validate request body
const body = await request.json();
const { listingId, adminId } = body;

// Input validation
if (!listingId || typeof listingId !== 'string') {
return NextResponse.json(
{ success: false, error: 'Listing ID is required' },
{ status: 400 }
);
}

if (!isValidUUID(listingId)) {
return NextResponse.json(
{ success: false, error: 'Invalid listing ID format' },
{ status: 400 }
);
}

console.log('Server-side approve listing:', { listingId, adminId });

// Verify admin status
const { data: adminData, error: adminError } = await supabaseAdmin
.from('users')
.select('is_admin')
.eq('id', adminId)
.single();

if (adminError || !adminData?.is_admin) {
console.error('Admin verification failed:', adminError);
return NextResponse.json(
{ success: false, error: 'Unauthorized: Only admins can approve listings' },
{ status: 403 }
);
}

// Verify listing exists before updating
const { data: existingListing, error: listingCheckError } = await supabaseAdmin
.from('listings')
.select('id, status')
.eq('id', listingId)
.single();

if (listingCheckError || !existingListing) {
return NextResponse.json(
{ success: false, error: 'Listing not found' },
{ status: 404 }
);
}

// Update listing status to approved
const { error: updateError } = await supabaseAdmin
.from('listings')
.update({
status: 'approved',
denial_reason: null
})
.eq('id', listingId);

if (updateError) {
console.error('Error approving listing:', updateError);
return NextResponse.json(
{ success: false, error: 'Failed to approve listing' },
{ status: 500 }
);
}

console.log('Listing approved successfully');
return NextResponse.json({ success: true });
} catch (error) {
console.error('Exception in approve listing API:', error);
return NextResponse.json(
{ success: false, error: 'Internal server error' },
{ status: 500 }
);
}
}
Loading
Loading