196 lines
6 KiB
TypeScript
196 lines
6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useI18n } from '../../components/I18nProvider';
|
|
|
|
type UserRow = {
|
|
id: string;
|
|
email: string;
|
|
name: string | null;
|
|
role: string;
|
|
status: string;
|
|
emailVerifiedAt: string | null;
|
|
approvedAt: string | null;
|
|
};
|
|
|
|
const roleOptions = ['USER', 'USER_MODERATOR', 'LISTING_MODERATOR', 'ADMIN'];
|
|
|
|
export default function AdminUsersPage() {
|
|
const { t } = useI18n();
|
|
const [users, setUsers] = useState<UserRow[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
async function load() {
|
|
setError(null);
|
|
try {
|
|
const res = await fetch('/api/admin/users', { cache: 'no-store' });
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to load users');
|
|
} else {
|
|
setUsers(data.users ?? []);
|
|
}
|
|
} catch (e) {
|
|
setError('Failed to load users');
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, []);
|
|
|
|
async function setRole(userId: string, role: string) {
|
|
setMessage(null);
|
|
setError(null);
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch('/api/admin/users/role', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ userId, role }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to update role');
|
|
} else {
|
|
setMessage(t('userUpdated'));
|
|
load();
|
|
}
|
|
} catch (e) {
|
|
setError('Failed to update role');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function approve(userId: string) {
|
|
setMessage(null);
|
|
setError(null);
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch('/api/admin/users/approve', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ userId }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to approve user');
|
|
} else {
|
|
setMessage(t('userUpdated'));
|
|
load();
|
|
}
|
|
} catch (e) {
|
|
setError('Failed to approve user');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function reject(userId: string) {
|
|
setMessage(null);
|
|
setError(null);
|
|
setLoading(true);
|
|
try {
|
|
const reason = window.prompt('Reason for rejection? (optional)');
|
|
const res = await fetch('/api/admin/users/reject', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ userId, reason }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to reject user');
|
|
} else {
|
|
setMessage(t('userUpdated'));
|
|
load();
|
|
}
|
|
} catch (e) {
|
|
setError('Failed to reject user');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function remove(userId: string) {
|
|
setMessage(null);
|
|
setError(null);
|
|
setLoading(true);
|
|
try {
|
|
const reason = window.prompt('Reason for removal? (optional)');
|
|
const res = await fetch('/api/admin/users/remove', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ userId, reason }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to remove user');
|
|
} else {
|
|
setMessage(t('userUpdated'));
|
|
load();
|
|
}
|
|
} catch (e) {
|
|
setError('Failed to remove user');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="panel" style={{ maxWidth: 960, margin: '40px auto' }}>
|
|
<h1>{t('adminUsersTitle')}</h1>
|
|
<p>{t('adminUsersLead')}</p>
|
|
{message ? <p style={{ color: 'green' }}>{message}</p> : null}
|
|
{error ? <p style={{ color: 'red' }}>{error}</p> : null}
|
|
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: 12 }}>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ textAlign: 'left', padding: 8 }}>{t('tableEmail')}</th>
|
|
<th style={{ textAlign: 'left', padding: 8 }}>{t('tableRole')}</th>
|
|
<th style={{ textAlign: 'left', padding: 8 }}>{t('tableStatus')}</th>
|
|
<th style={{ textAlign: 'left', padding: 8 }}>{t('tableVerified')}</th>
|
|
<th style={{ textAlign: 'left', padding: 8 }}>{t('tableApproved')}</th>
|
|
<th style={{ textAlign: 'left', padding: 8 }}>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map((u) => (
|
|
<tr key={u.id} style={{ borderTop: '1px solid #eee' }}>
|
|
<td style={{ padding: 8 }}>{u.email}</td>
|
|
<td style={{ padding: 8 }}>
|
|
<select value={u.role} onChange={(e) => setRole(u.id, e.target.value)} disabled={loading}>
|
|
{roleOptions.map((r) => (
|
|
<option key={r} value={r}>
|
|
{r}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</td>
|
|
<td style={{ padding: 8 }}>{u.status}</td>
|
|
<td style={{ padding: 8 }}>{u.emailVerifiedAt ? 'yes' : 'no'}</td>
|
|
<td style={{ padding: 8 }}>{u.approvedAt ? 'yes' : 'no'}</td>
|
|
<td style={{ padding: 8 }}>
|
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
{u.approvedAt ? null : (
|
|
<button className="button secondary" onClick={() => approve(u.id)} disabled={loading}>
|
|
{t('approve')}
|
|
</button>
|
|
)}
|
|
<button className="button secondary" onClick={() => reject(u.id)} disabled={loading}>
|
|
{t('reject')}
|
|
</button>
|
|
<button className="button secondary" onClick={() => remove(u.id)} disabled={loading}>
|
|
{t('remove')}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</main>
|
|
);
|
|
}
|