forge/frontend/app/admin/users/page.tsx
DJP 7a804e896d Initial commit - FORGE AI unified platform
Features:
- Image generation (OpenAI, Gemini, Leonardo, Bria, Stability, Flux)
- Nano Banana iterative editing
- Video generation and upscaling
- Audio TTS, STT, sound effects (ElevenLabs)
- Text prompt studio and alt text
- User authentication with JWT/cookies
- Admin panel with voice management
- Job queue with Celery
- PostgreSQL + Redis backend
- Next.js 15 + FastAPI architecture

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2025-12-09 20:39:00 -05:00

306 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { Users, Search, Edit2, Shield, ShieldOff, Trash2 } from 'lucide-react';
import AdminGuard from '@/components/AdminGuard';
import api from '@/lib/api';
interface User {
id: string;
email: string;
name: string;
role: string;
is_active: boolean;
created_at: string;
last_login?: string;
}
export default function UserManagementPage() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [roleFilter, setRoleFilter] = useState('');
const [editingUser, setEditingUser] = useState<User | null>(null);
const [newRole, setNewRole] = useState('');
useEffect(() => {
fetchUsers();
}, [roleFilter]);
const fetchUsers = async () => {
setLoading(true);
try {
const params: any = {};
if (roleFilter) params.role = roleFilter;
const response = await api.get('/admin/users', { params });
setUsers(response.data.items || []);
} catch (err) {
// Mock data for demo
setUsers([
{
id: '1',
email: 'admin@forgeai.dev',
name: 'Admin User',
role: 'admin',
is_active: true,
created_at: '2024-01-15T10:00:00Z',
last_login: '2024-12-09T14:30:00Z',
},
{
id: '2',
email: 'test@forgeai.dev',
name: 'Test User',
role: 'user',
is_active: true,
created_at: '2024-02-01T10:00:00Z',
last_login: '2024-12-09T12:00:00Z',
},
{
id: '3',
email: 'john@example.com',
name: 'John Doe',
role: 'user',
is_active: true,
created_at: '2024-03-01T10:00:00Z',
},
]);
} finally {
setLoading(false);
}
};
const handleUpdateRole = async () => {
if (!editingUser || !newRole) return;
try {
await api.patch(`/admin/users/${editingUser.id}`, { role: newRole });
toast.success('User role updated');
setEditingUser(null);
fetchUsers();
} catch (err) {
toast.error('Failed to update role');
}
};
const handleToggleActive = async (user: User) => {
try {
await api.patch(`/admin/users/${user.id}`, { is_active: !user.is_active });
toast.success(user.is_active ? 'User deactivated' : 'User activated');
fetchUsers();
} catch (err) {
toast.error('Failed to update user status');
}
};
const filteredUsers = users.filter(
(user) =>
user.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'super_admin':
return 'bg-red-900/50 text-red-400';
case 'admin':
return 'bg-orange-900/50 text-orange-400';
default:
return 'bg-blue-900/50 text-blue-400';
}
};
return (
<AdminGuard>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-blue-900/30 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-blue-400" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">User Management</h1>
<p className="text-gray-500">Manage users and their roles</p>
</div>
</div>
</div>
{/* Filters */}
<div className="flex gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search users..."
className="input-field pl-10"
/>
</div>
</div>
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="select-field w-40"
>
<option value="">All Roles</option>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="super_admin">Super Admin</option>
</select>
</div>
{/* Users Table */}
<div className="bg-forge-dark rounded-xl border border-gray-800 overflow-hidden">
{loading ? (
<div className="p-8 text-center text-gray-500">Loading...</div>
) : filteredUsers.length === 0 ? (
<div className="p-8 text-center text-gray-500">No users found</div>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-gray-800">
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
User
</th>
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
Role
</th>
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
Status
</th>
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
Last Login
</th>
<th className="text-right px-6 py-4 text-sm font-medium text-gray-500">
Actions
</th>
</tr>
</thead>
<tbody>
{filteredUsers.map((user) => (
<tr
key={user.id}
className="border-b border-gray-800 last:border-0 hover:bg-forge-gray/50"
>
<td className="px-6 py-4">
<div>
<p className="text-white font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.email}</p>
</div>
</td>
<td className="px-6 py-4">
<span className={`badge ${getRoleBadgeColor(user.role)}`}>
{user.role.replace('_', ' ')}
</span>
</td>
<td className="px-6 py-4">
<span
className={`badge ${
user.is_active
? 'bg-green-900/50 text-green-400'
: 'bg-gray-700 text-gray-400'
}`}
>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-6 py-4 text-gray-400 text-sm">
{user.last_login
? new Date(user.last_login).toLocaleDateString()
: 'Never'}
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => {
setEditingUser(user);
setNewRole(user.role);
}}
className="p-2 text-gray-400 hover:text-forge-yellow transition-colors"
title="Edit role"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleToggleActive(user)}
className={`p-2 transition-colors ${
user.is_active
? 'text-gray-400 hover:text-red-400'
: 'text-gray-400 hover:text-green-400'
}`}
title={user.is_active ? 'Deactivate' : 'Activate'}
>
{user.is_active ? (
<ShieldOff className="w-4 h-4" />
) : (
<Shield className="w-4 h-4" />
)}
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Edit Role Modal */}
{editingUser && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-forge-dark rounded-xl border border-gray-800 w-full max-w-md">
<div className="p-6 border-b border-gray-800 flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Change User Role</h3>
<button
onClick={() => setEditingUser(null)}
className="text-gray-400 hover:text-white"
>
&times;
</button>
</div>
<div className="p-6 space-y-4">
<div>
<p className="text-gray-400 text-sm mb-1">User</p>
<p className="text-white">{editingUser.name}</p>
<p className="text-sm text-gray-500">{editingUser.email}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
New Role
</label>
<select
value={newRole}
onChange={(e) => setNewRole(e.target.value)}
className="select-field"
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="super_admin">Super Admin</option>
</select>
</div>
<div className="flex gap-3">
<button
onClick={() => setEditingUser(null)}
className="btn-secondary flex-1"
>
Cancel
</button>
<button
onClick={handleUpdateRole}
className="btn-primary flex-1"
>
Update Role
</button>
</div>
</div>
</div>
</div>
)}
</div>
</AdminGuard>
);
}