![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/private_html/src/pages/admin/ |
import { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import LayoutWithSidebar from '@/components/LayoutWithSidebar';
import { canAccessAdmin, isSuperAdmin } from '@/lib/auth-utils';
import { useImpersonation } from '@/hooks/useImpersonation';
interface User {
id: string;
email: string;
name: string | null;
role: 'USER' | 'ADMIN';
createdAt: string;
updatedAt: string;
}
export default function AdminUsersPage() {
const { data: session, status } = useSession();
const router = useRouter();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [showPasswordReset, setShowPasswordReset] = useState(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [resetting, setResetting] = useState(false);
const [success, setSuccess] = useState('');
const [isMobile, setIsMobile] = useState(false);
const { impersonateUser, stopImpersonation, isImpersonating, isCurrentlyImpersonating, originalUser } = useImpersonation();
// Mobile detection
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/login');
return;
}
if (!canAccessAdmin(session)) {
router.push('/user/dashboard');
return;
}
}, [status, router]);
useEffect(() => {
const fetchUsers = async () => {
if (status !== 'authenticated' || !canAccessAdmin(session)) {
return;
}
try {
const response = await fetch('/api/admin/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
setUsers(data.users || data); // Handle both new and old API format
} catch (err) {
setError('Error loading users');
console.error(err);
} finally {
setLoading(false);
}
};
fetchUsers();
}, [status, session]);
const handleRoleChange = async (userId: string, newRole: 'USER' | 'ADMIN') => {
try {
const response = await fetch(`/api/admin/users/${userId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ role: newRole }),
});
if (!response.ok) {
throw new Error('Failed to update user role');
}
const updatedUser = await response.json();
setUsers(users.map(user =>
user.id === updatedUser.id ? updatedUser : user
));
} catch (err) {
setError('Error updating user role');
console.error(err);
}
};
const handleDeleteUser = async (userId: string) => {
if (!confirm('Are you sure you want to delete this user?')) {
return;
}
try {
const response = await fetch(`/api/admin/users/${userId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete user');
}
setUsers(users.filter(user => user.id !== userId));
} catch (err) {
setError('Error deleting user');
console.error(err);
}
};
const handlePasswordReset = (user: User) => {
setSelectedUser(user);
setNewPassword('');
setConfirmPassword('');
setShowPasswordReset(true);
setError('');
setSuccess('');
};
const handlePasswordSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedUser) return;
if (newPassword !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (newPassword.length < 6) {
setError('Password must be at least 6 characters long');
return;
}
setResetting(true);
setError('');
try {
const response = await fetch('/api/admin/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: selectedUser.id,
newPassword: newPassword,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to reset password');
}
setSuccess(`Password reset successfully for ${selectedUser.email}`);
setNewPassword('');
setConfirmPassword('');
// Close modal after 2 seconds
setTimeout(() => {
setShowPasswordReset(false);
setSuccess('');
}, 2000);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error resetting password');
} finally {
setResetting(false);
}
};
if (status === 'loading') {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Loading...</span>
</div>
);
}
if (!session || !canAccessAdmin(session)) {
return null;
}
return (
<LayoutWithSidebar>
<div className={`mx-auto ${isMobile ? 'px-2 py-4 max-w-full' : 'max-w-6xl px-4 py-8'}`}>
{/* Back Button */}
<div className="mb-6">
<button
onClick={() => router.push('/admin')}
className={`inline-flex items-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors ${isMobile ? 'px-3 py-2 text-sm' : 'px-4 py-2'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className={`mr-2 ${isMobile ? 'h-4 w-4' : 'h-5 w-5'}`} viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L7.414 9H17a1 1 0 110 2H7.414l2.293 2.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
Back to Dashboard
</button>
</div>
{/* Header */}
<div className={`bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl shadow-xl mb-8 ${isMobile ? 'p-6' : 'p-8'}`}>
<div className="flex justify-between items-start">
<div>
<h1 className={`font-bold ${isMobile ? 'text-2xl' : 'text-3xl'}`}>User Management</h1>
<p className={`mt-2 ${isMobile ? 'text-sm' : ''}`}>Manage user accounts, roles, and permissions</p>
</div>
{/* Stop Impersonation Button */}
{isCurrentlyImpersonating && originalUser && (
<button
onClick={stopImpersonation}
className={`bg-red-500 text-white rounded-lg font-semibold hover:bg-red-600 transition-all duration-200 ${isMobile ? 'px-3 py-2 text-sm' : 'px-4 py-2'}`}
title={`Stop impersonating ${session?.user?.name || session?.user?.email}`}
>
🚫 Stop Impersonation
</button>
)}
</div>
{/* Impersonation Status */}
{isCurrentlyImpersonating && originalUser && (
<div className={`mt-4 p-3 bg-yellow-500 bg-opacity-20 rounded-lg ${isMobile ? 'text-sm' : ''}`}>
<p className="font-semibold">🔄 Currently impersonating: {session?.user?.name || session?.user?.email}</p>
<p className="text-yellow-100">Original user: {originalUser.name || originalUser.email}</p>
</div>
)}
</div>
{error && (
<div className={`bg-red-100 border border-red-400 text-red-700 rounded mb-4 ${isMobile ? 'px-3 py-2 text-sm' : 'px-4 py-3'}`}>
{error}
</div>
)}
{/* Users Grid */}
<div className={`grid gap-6 ${isMobile ? 'grid-cols-1' : 'md:grid-cols-2 lg:grid-cols-3'}`}>
{users.map(user => (
<div key={user.id} className={`bg-white rounded-xl shadow-xl border border-gray-200 ${isMobile ? 'p-6' : 'p-8'}`}>
{/* User Info */}
<div className="mb-4">
<div className={`flex items-center mb-2 ${isMobile ? 'flex-col text-center' : ''}`}>
<div className={`bg-blue-500 text-white rounded-full flex items-center justify-center ${isMobile ? 'w-12 h-12 mb-2' : 'w-10 h-10 mr-3'}`}>
{user.name ? user.name.charAt(0).toUpperCase() : user.email.charAt(0).toUpperCase()}
</div>
<div className={`flex-1 ${isMobile ? 'text-center' : ''}`}>
<h3 className={`font-bold text-gray-900 ${isMobile ? 'text-lg' : 'text-xl'}`}>
{user.name || 'No Name'}
</h3>
<p className={`text-gray-600 break-all ${isMobile ? 'text-sm' : ''}`}>{user.email}</p>
</div>
</div>
</div>
{/* Role Badge */}
<div className="mb-4">
<span className={`px-3 py-1 rounded-full text-xs font-bold ${
user.role === 'ADMIN'
? 'bg-purple-100 text-purple-800'
: 'bg-green-100 text-green-800'
}`}>
{user.role}
</span>
</div>
{/* User Stats */}
<div className={`mb-4 text-gray-600 ${isMobile ? 'text-xs' : 'text-sm'}`}>
<p>Created: {new Date(user.createdAt).toLocaleDateString()}</p>
<p>Updated: {new Date(user.updatedAt).toLocaleDateString()}</p>
</div>
{/* Role Change */}
<div className="mb-4">
<label className={`block text-gray-700 font-semibold mb-2 ${isMobile ? 'text-sm' : ''}`}>
Change Role
</label>
<select
value={user.role}
onChange={(e) => handleRoleChange(user.id, e.target.value as 'USER' | 'ADMIN')}
className={`w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${isMobile ? 'px-3 py-2 text-sm' : 'px-4 py-2'}`}
>
<option value="USER">User</option>
<option value="ADMIN">Admin</option>
</select>
</div>
{/* Actions */}
<div className={`space-y-2 ${isMobile ? '' : 'space-y-0 space-x-2 flex flex-wrap'}`}>
{/* Sign In As Button - Only for SUPERADMIN */}
{isSuperAdmin(session) && user.id !== session.user.id && (
<button
onClick={() => impersonateUser(user.id, user.name || user.email)}
disabled={isImpersonating || session.user.isImpersonating}
className={`bg-purple-500 text-white rounded-lg font-semibold hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 ${isMobile ? 'w-full px-4 py-2 text-sm mb-2' : 'flex-1 px-3 py-2 text-sm'}`}
title={session.user.isImpersonating ? 'Stop current impersonation first' : 'Sign in as this user'}
>
{isImpersonating ? '🔄 Switching...' : session.user.isImpersonating ? '🔄 Currently Impersonating' : '🔄 Sign In As'}
</button>
)}
<button
onClick={() => handlePasswordReset(user)}
className={`bg-yellow-500 text-white rounded-lg font-semibold hover:bg-yellow-600 transition-all duration-200 ${isMobile ? 'w-full px-4 py-2 text-sm' : 'flex-1 px-4 py-2'}`}
>
Reset Password
</button>
<button
onClick={() => handleDeleteUser(user.id)}
className={`bg-red-500 text-white rounded-lg font-semibold hover:bg-red-600 transition-all duration-200 ${isMobile ? 'w-full px-4 py-2 text-sm' : 'flex-1 px-4 py-2'}`}
>
Delete User
</button>
</div>
</div>
))}
</div>
{users.length === 0 && (
<div className={`text-center ${isMobile ? 'py-8' : 'py-12'}`}>
<div className={`text-gray-400 mb-4 ${isMobile ? 'text-4xl' : 'text-6xl'}`}>👥</div>
<h3 className={`font-bold text-gray-700 mb-2 ${isMobile ? 'text-lg' : 'text-xl'}`}>No Users Found</h3>
<p className={`text-gray-600 ${isMobile ? 'text-sm' : ''}`}>There are no users in the system yet.</p>
</div>
)}
{/* Password Reset Modal */}
{showPasswordReset && selectedUser && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className={`bg-white rounded-xl shadow-xl ${isMobile ? 'w-full max-w-sm p-6' : 'max-w-md w-full p-8'}`}>
<h2 className={`font-bold text-gray-900 mb-4 ${isMobile ? 'text-lg' : 'text-xl'}`}>
Reset Password for {selectedUser.email}
</h2>
{success && (
<div className={`bg-green-100 border border-green-400 text-green-700 rounded mb-4 ${isMobile ? 'px-3 py-2 text-sm' : 'px-4 py-3'}`}>
{success}
</div>
)}
{error && (
<div className={`bg-red-100 border border-red-400 text-red-700 rounded mb-4 ${isMobile ? 'px-3 py-2 text-sm' : 'px-4 py-3'}`}>
{error}
</div>
)}
<form onSubmit={handlePasswordSubmit} className="space-y-4">
<div>
<label className={`block text-gray-700 font-semibold mb-2 ${isMobile ? 'text-sm' : ''}`}>
New Password
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={6}
className={`w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${isMobile ? 'px-3 py-2 text-sm' : 'px-4 py-2'}`}
placeholder="Enter new password"
/>
</div>
<div>
<label className={`block text-gray-700 font-semibold mb-2 ${isMobile ? 'text-sm' : ''}`}>
Confirm Password
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={6}
className={`w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${isMobile ? 'px-3 py-2 text-sm' : 'px-4 py-2'}`}
placeholder="Confirm new password"
/>
</div>
<div className={`space-y-2 ${isMobile ? '' : 'space-y-0 space-x-4 flex'}`}>
<button
type="submit"
disabled={resetting}
className={`bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 ${isMobile ? 'w-full px-4 py-2 text-sm' : 'flex-1 px-4 py-2'}`}
>
{resetting ? 'Resetting...' : 'Reset Password'}
</button>
<button
type="button"
onClick={() => setShowPasswordReset(false)}
className={`bg-gray-600 text-white rounded-lg font-semibold hover:bg-gray-700 transition-all duration-200 ${isMobile ? 'w-full px-4 py-2 text-sm' : 'flex-1 px-4 py-2'}`}
>
Cancel
</button>
</div>
</form>
</div>
</div>
)}
</div>
</LayoutWithSidebar>
);
}