Auth (hand-built, uses the auth core): - /register + register() action: validates, hashes with argon2id (hashPassword), creates an emulator-compatible users row (accountCreated/ipRegister/ipCurrent/ look), redirect kept outside try so NEXT_REDIRECT propagates. Login/register cross-links; header shows Register (logged-out) and a Logout (signOut) button. Batch 5 (parallel agents): /admin/rooms (+[id]) search+detail, /admin/vouchers (CRUD), /admin/subscriptions (read), /admin/calendar (campaigns+rewards read), /marketplace (public). Header + admin nav extended. Verified: tsc exit 0, vitest 48/48, next build exit 0 (52 page routes).
75 lines
2.0 KiB
TypeScript
75 lines
2.0 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type SubRow = {
|
|
id: number;
|
|
userId: number | null;
|
|
subscriptionType: string | null;
|
|
expiresAt: number;
|
|
};
|
|
|
|
function fmt(ts: number): string {
|
|
return new Date(ts * 1000).toISOString().slice(0, 16).replace("T", " ");
|
|
}
|
|
|
|
export default async function AdminSubscriptions() {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
let active: SubRow[] = [];
|
|
try {
|
|
// Schema has no timestamp_end column; expiry = timestamp_start + duration.
|
|
// Active subs are active=1 with a computed expiry still in the future.
|
|
const rows = await prisma.usersSubscriptions.findMany({
|
|
where: { active: 1 },
|
|
orderBy: { id: "desc" },
|
|
take: 500,
|
|
});
|
|
|
|
active = rows
|
|
.map((r) => ({
|
|
id: r.id,
|
|
userId: r.userId,
|
|
subscriptionType: r.subscriptionType,
|
|
expiresAt: (r.timestampStart ?? 0) + (r.duration ?? 0),
|
|
}))
|
|
.filter((r) => r.expiresAt > now)
|
|
.sort((a, b) => a.expiresAt - b.expiresAt);
|
|
} catch {
|
|
active = [];
|
|
}
|
|
|
|
return (
|
|
<main>
|
|
<h1>Subscriptions</h1>
|
|
<p className="muted" style={{ marginTop: "-0.25rem", marginBottom: "1rem" }}>
|
|
Active Habbo Club / VIP subscriptions (expiry > now). Read-only.
|
|
</p>
|
|
|
|
<div className="card">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Sub ID</th>
|
|
<th>User ID</th>
|
|
<th>Type</th>
|
|
<th>Expires</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{active.map((s) => (
|
|
<tr key={s.id}>
|
|
<td>{s.id}</td>
|
|
<td>{s.userId ?? "—"}</td>
|
|
<td>{s.subscriptionType ?? "—"}</td>
|
|
<td className="muted">{fmt(s.expiresAt)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{active.length === 0 ? <p className="muted">No active subscriptions.</p> : null}
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|