231 lines
6.9 KiB
TypeScript
231 lines
6.9 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 [role, setRole] = useState<string | null>(null);
|
|
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(() => {
|
|
fetch("/api/auth/me", { cache: "no-store" })
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
setRole(data.user?.role ?? null);
|
|
if (data.user?.role === "ADMIN") {
|
|
load();
|
|
} else {
|
|
setError(t("adminRequired"));
|
|
}
|
|
})
|
|
.catch(() => setError(t("adminRequired")));
|
|
}, [t]);
|
|
|
|
async function setUserRole(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) => setUserRole(u.id, e.target.value)}
|
|
disabled={loading || role !== "ADMIN"}
|
|
>
|
|
{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 || role !== "ADMIN"}
|
|
>
|
|
{t("approve")}
|
|
</button>
|
|
)}
|
|
<button
|
|
className="button secondary"
|
|
onClick={() => reject(u.id)}
|
|
disabled={loading || role !== "ADMIN"}
|
|
>
|
|
{t("reject")}
|
|
</button>
|
|
<button
|
|
className="button secondary"
|
|
onClick={() => remove(u.id)}
|
|
disabled={loading || role !== "ADMIN"}
|
|
>
|
|
{t("remove")}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</main>
|
|
);
|
|
}
|