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:
Simo committed 2026-06-28 13:52:32 +02:00
1 parent e8be0461d8
commit 08a9882be8
13 files changed
+1001 -4

No files matched your search

+74
View File
@@ -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 &gt; 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>
);
}