lomavuokraus/app/admin/users/page.tsx
Tero Halla-aho 0bb709d9c5
Some checks failed
CI / checks (push) Has been cancelled
chore: fix audit alerts and formatting
2026-02-04 12:43:03 +02:00

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>
);
}