Add register + logout auth flows, and batch-5 resources
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).
This commit is contained in:
1 parent
e8be0461d8
commit
08a9882be8
13 files changed
+1001
-4
No files matched your search
@@ -0,0 +1,74 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user