Files
Epicnabbo-Catalogus-Updated…/src/app/settings/page.tsx
T
simoleo e668fa85ec Add 2FA, email + password reset, and batch-7 pages
Auth (hand-built on the auth core):
- 2FA: User model gains two_factor_secret/recovery_codes/confirmed_at (+ idempotent
  MariaDB migration). authorize() requires a valid TOTP code when 2FA is confirmed
  (secret decrypted via Laravel APP_KEY, fail-closed). Two-step login (precheckLogin
  reveals the code field). /settings/2fa enable/confirm/disable flow.
- Password reset: nodemailer email service; PasswordReset model + migration;
  /forgot (request, generic response) + /reset (token sha256 + 1h TTL, sets argon2id
  hash). Login links to forgot.

Batch 7 (parallel agents): /admin/commandocentrum (RCON controls + emulator_errors),
social write actions (friend request + guild forum new thread), /help/[category],
/badges (public). env: APP_KEY, APP_URL, SMTP_*. Nav extended.

Verified: tsc exit 0, vitest 48/48, next build exit 0 (64 page routes).
2026-06-28 14:25:19 +02:00

98 lines
2.9 KiB
TypeScript

import Link from "next/link";
import { redirect } from "next/navigation";
import { updateMotto } from "@/actions/user-settings";
import { avatarImageUrl } from "@/lib/format";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
export default async function SettingsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const id = Number(session.user.id);
let user: {
username: string;
motto: string;
look: string;
mail: string | null;
} | null = null;
try {
user = await prisma.user.findUnique({
where: { id },
select: { username: true, motto: true, look: true, mail: true },
});
} catch {
user = null;
}
if (!user) redirect("/login");
let imagerBase = "https://www.habbo.com/habbo-imaging/avatarimage";
try {
imagerBase =
(await siteSettings.get("habbo_imaging_url", imagerBase)) ?? imagerBase;
} catch {
// fall back to the default imager base
}
const avatar = avatarImageUrl(imagerBase, user.look, { size: "l" });
return (
<main>
<h1>Account settings</h1>
<div
className="card"
style={{ display: "flex", gap: "1.5rem", alignItems: "center", marginBottom: "1.5rem" }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="avatar" src={avatar} alt={`${user.username} avatar`} width={100} height={150} />
<div>
<h2 style={{ margin: "0 0 0.25rem" }}>{user.username}</h2>
<p style={{ margin: "0 0 0.5rem" }}>
{user.motto || <span className="muted">No motto</span>}
</p>
<p className="muted" style={{ margin: 0 }}>
{user.mail || "No e-mail on file"}
</p>
</div>
</div>
<div className="card" style={{ maxWidth: 520 }}>
<h3 style={{ marginTop: 0 }}>Change motto</h3>
<form action={updateMotto} style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<input
name="motto"
defaultValue={user.motto}
maxLength={127}
placeholder="Your motto"
aria-label="Motto"
/>
<div>
<button type="submit" className="btn btn-primary">
Save motto
</button>
</div>
</form>
<p className="muted" style={{ marginBottom: 0 }}>
Updates instantly in-game if you are online.
</p>
</div>
<div className="card" style={{ maxWidth: 520, marginTop: "1.5rem" }}>
<h3 style={{ marginTop: 0 }}>Security</h3>
<p className="muted" style={{ marginTop: 0 }}>
Protect your account with two-factor authentication.
</p>
<Link href="/settings/2fa" className="btn btn-outline">
Two-factor authentication
</Link>
</div>
</main>
);
}