/news and six admin pages issued raw prisma reads with no try/catch, so a DB outage rendered a 500 instead of an empty state. Wrap each read (try/catch or .catch(() => fallback), preserving select() row types and notFound() on the user-detail page). Matches the fail-soft pattern used across the rest of the app. Verified: tsc 0, next build 0, all public pages 200 against a dead DB.
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
import { createBan, liftBan } from "@/actions/admin-bans";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function AdminBans() {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
let bans: Awaited<ReturnType<typeof prisma.ban.findMany>> = [];
|
|
try {
|
|
bans = await prisma.ban.findMany({
|
|
where: { banExpire: { gt: now } },
|
|
orderBy: { timestamp: "desc" },
|
|
take: 100,
|
|
});
|
|
} catch {
|
|
bans = [];
|
|
}
|
|
|
|
return (
|
|
<main>
|
|
<h1>Bans</h1>
|
|
|
|
<form action={createBan} className="card" style={{ marginBottom: "1.5rem" }}>
|
|
<h3 style={{ marginTop: 0 }}>New ban</h3>
|
|
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
|
<input name="userId" type="number" min={1} placeholder="User ID" required />
|
|
<select name="type">
|
|
<option value="account">account</option>
|
|
<option value="ip">ip</option>
|
|
<option value="machine">machine</option>
|
|
<option value="super">super</option>
|
|
</select>
|
|
<input name="hours" type="number" min={0} placeholder="Hours (0 = perm)" />
|
|
<input name="reason" placeholder="Reason" style={{ flex: 1, minWidth: 160 }} />
|
|
<button type="submit">Ban</button>
|
|
</div>
|
|
</form>
|
|
|
|
<table style={{ width: "100%", borderCollapse: "collapse" }}>
|
|
<thead>
|
|
<tr style={{ textAlign: "left", color: "var(--muted)" }}>
|
|
<th>User ID</th>
|
|
<th>Type</th>
|
|
<th>Reason</th>
|
|
<th>Expires</th>
|
|
<th />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{bans.map((b) => (
|
|
<tr key={b.id} style={{ borderTop: "1px solid var(--border)" }}>
|
|
<td>{b.userId}</td>
|
|
<td>{b.type}</td>
|
|
<td>{b.banReason}</td>
|
|
<td className="muted">{new Date(b.banExpire * 1000).toISOString().slice(0, 16).replace("T", " ")}</td>
|
|
<td>
|
|
<form action={liftBan}>
|
|
<input type="hidden" name="id" value={b.id} />
|
|
<button type="submit">Lift</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{bans.length === 0 ? <p className="muted">No active bans.</p> : null}
|
|
</main>
|
|
);
|
|
}
|