lomavuokraus/app/admin/pending/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

276 lines
8.9 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useI18n } from "../../components/I18nProvider";
type PendingUser = {
id: string;
email: string;
status: string;
emailVerifiedAt: string | null;
approvedAt: string | null;
role: string;
};
type PendingListing = {
id: string;
status: string;
createdAt: string;
owner: { email: string };
translations: { title: string; slug: string; locale: string }[];
evChargingAvailable: boolean;
evChargingOnSite: boolean;
wheelchairAccessible: boolean;
};
export default function PendingAdminPage() {
const { t } = useI18n();
const [pendingUsers, setPendingUsers] = useState<PendingUser[]>([]);
const [pendingListings, setPendingListings] = useState<PendingListing[]>([]);
const [role, setRole] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function loadPending() {
setMessage(null);
setError(null);
try {
const res = await fetch("/api/admin/pending", { cache: "no-store" });
const data = await res.json();
if (!res.ok) {
setError(data.error || "Failed to load");
return;
}
setRole(data.role ?? null);
setPendingUsers(data.users ?? []);
setPendingListings(data.listings ?? []);
} catch (e) {
setError("Failed to load");
}
}
useEffect(() => {
fetch("/api/auth/me", { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
setRole(data.user?.role ?? null);
if (!data.user?.role) {
setError(t("adminRequired"));
return;
}
if (
["ADMIN", "USER_MODERATOR", "LISTING_MODERATOR"].includes(
data.user.role,
)
) {
loadPending();
return;
}
setError(t("adminRequired"));
})
.catch(() => setError(t("adminRequired")));
}, [t]);
async function approveUser(userId: string, makeAdmin: boolean) {
setMessage(null);
setError(null);
const res = await fetch("/api/admin/users/approve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId, makeAdmin }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || "Failed to approve user");
} else {
setMessage(t("userUpdated"));
loadPending();
}
}
async function approveListing(
listingId: string,
action: "approve" | "reject" | "remove",
) {
setMessage(null);
setError(null);
const reason =
action === "reject"
? window.prompt(`${t("reject")}? (optional)`)
: action === "remove"
? window.prompt(`${t("remove")}? (optional)`)
: null;
const res = await fetch("/api/admin/listings/approve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ listingId, action, reason }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || "Failed to update listing");
} else {
setMessage(t("approvalsMessage"));
loadPending();
}
}
async function rejectUser(userId: string) {
setMessage(null);
setError(null);
const reason = window.prompt(`${t("reject")}? (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"));
loadPending();
}
}
return (
<main className="panel" style={{ maxWidth: 960, margin: "40px auto" }}>
<h1>{t("pendingAdminTitle")}</h1>
<div style={{ display: "grid", gap: 16 }}>
<section>
<h3>{t("pendingUsersTitle")}</h3>
{pendingUsers.length === 0 ? (
<p>{t("noPendingUsers")}</p>
) : (
<ul
style={{ display: "grid", gap: 8, padding: 0, listStyle: "none" }}
>
{pendingUsers.map((u) => (
<li
key={u.id}
style={{
border: "1px solid #ddd",
borderRadius: 8,
padding: 12,
}}
>
<div>
<strong>{u.email}</strong> {t("statusLabel")}: {u.status}{" "}
{t("verifiedLabel")}:{" "}
{u.emailVerifiedAt ? t("yes") : t("no")}
</div>
<div style={{ marginTop: 8, display: "flex", gap: 8 }}>
<button
className="button"
onClick={() => approveUser(u.id, false)}
disabled={role !== "ADMIN" && role !== "USER_MODERATOR"}
>
{t("approve")}
</button>
<button
className="button secondary"
onClick={() => approveUser(u.id, true)}
disabled={role !== "ADMIN"}
>
{t("approveAdmin")}
</button>
<button
className="button secondary"
onClick={() => rejectUser(u.id)}
disabled={role !== "ADMIN" && role !== "USER_MODERATOR"}
>
{t("reject")}
</button>
</div>
</li>
))}
</ul>
)}
</section>
<section>
<h3>{t("pendingListingsTitle")}</h3>
{pendingListings.length === 0 ? (
<p>{t("noPendingListings")}</p>
) : (
<ul
style={{ display: "grid", gap: 8, padding: 0, listStyle: "none" }}
>
{pendingListings.map((l) => (
<li
key={l.id}
style={{
border: "1px solid #ddd",
borderRadius: 8,
padding: 12,
}}
>
<div>
<strong>{l.translations[0]?.title ?? "Listing"}</strong>
owner: {l.owner.email}
</div>
<div
style={{
marginTop: 6,
display: "flex",
gap: 6,
flexWrap: "wrap",
}}
>
{l.evChargingOnSite ? (
<span className="badge">{t("amenityEvOnSite")}</span>
) : null}
{l.evChargingAvailable && !l.evChargingOnSite ? (
<span className="badge">{t("amenityEvNearby")}</span>
) : null}
{l.wheelchairAccessible ? (
<span className="badge">
{t("amenityWheelchairAccessible")}
</span>
) : null}
</div>
<div style={{ fontSize: 12, color: "#666" }}>
{t("slugsLabel")}:{" "}
{l.translations
.map((t) => `${t.slug} (${t.locale})`)
.join(", ")}
</div>
<div style={{ marginTop: 8, display: "flex", gap: 8 }}>
<button
className="button"
onClick={() => approveListing(l.id, "approve")}
disabled={
role !== "ADMIN" && role !== "LISTING_MODERATOR"
}
>
{t("publish")}
</button>
<button
className="button secondary"
onClick={() => approveListing(l.id, "reject")}
disabled={
role !== "ADMIN" && role !== "LISTING_MODERATOR"
}
>
{t("reject")}
</button>
<button
className="button secondary"
onClick={() => approveListing(l.id, "remove")}
disabled={
role !== "ADMIN" && role !== "LISTING_MODERATOR"
}
>
{t("remove")}
</button>
</div>
</li>
))}
</ul>
)}
</section>
</div>
{message ? (
<p style={{ marginTop: 12, color: "green" }}>{message}</p>
) : null}
{error ? <p style={{ marginTop: 12, color: "red" }}>{error}</p> : null}
</main>
);
}