fix: production hardening — migration script, security fixes, structured logging, API docs, component splitting

- Create apply-migrations.ts and jobs-worker.ts scripts (package.json references)
- Convert badge leaderboard from $queryRawUnsafe to $queryRaw with Prisma.sql templates
- Fix OAuth email binding: add oauth_require_link site setting, skip 2FA-protected accounts
- Add per-user 2FA rate limiting (5/30s) to prevent TOTP brute-force
- Add structured JSON logger with levels (debug/info/warn/error)
- Split 341-line HomePage into GuestView + UserView components
- Add OpenAPI v3.1 spec at /api/openapi.json
- Add LOG_LEVEL env var, regenerate Prisma client
- Add mysql2 dependency for migration scripts
- All 58 tests pass, typecheck clean
This commit is contained in:
openhands committed 2026-07-08 13:06:02 +02:00
1 parent 5c638cd6bc
commit c5db7f5156
17 files changed
+1175 -449

No files matched your search

+183
View File
@@ -0,0 +1,183 @@
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { avatarImageUrl, excerpt } from "@/lib/format";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
import { HomeLoginForm } from "@/components/auth/home-login-form";
export default async function GuestView() {
const t = await getTranslations("pages.home");
const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
const imager =
(await siteSettings.get("habbo_imaging_url", "https://www.habbo.com/habbo-imaging/avatarimage")) ?? "";
let articles: Array<{ slug: string; title: string; shortStory: string; image: string }> = [];
try {
articles = await prisma.websiteArticles.findMany({
orderBy: { createdAt: "desc" },
take: 8,
select: { slug: true, title: true, shortStory: true, image: true },
});
} catch {}
let recentUsers: Array<{ username: string; look: string }> = [];
try {
recentUsers = await prisma.user.findMany({
orderBy: { lastOnline: "desc" },
take: 12,
select: { username: true, look: true },
});
} catch {}
return (
<div className="flex flex-col gap-4 md:flex-row">
<div className="flex w-full flex-col gap-4 md:w-1/3 md:flex-shrink-0">
<div
className="overflow-hidden rounded-lg border"
style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 22%, transparent)" }}
>
<div className="relative h-[120px] overflow-hidden md:h-[150px]">
<div
className="absolute inset-0"
style={{
backgroundImage: "url(/assets/images/kasja_mepage_header.png)",
backgroundSize: "cover",
backgroundPosition: "center 30%",
backgroundRepeat: "no-repeat",
}}
/>
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.15) 50%, rgba(0,0,0,0.5) 100%)",
}}
/>
<div
className="absolute -bottom-8 right-4 w-32 h-32 rounded-full opacity-[0.12] blur-[60px]"
style={{ background: "var(--color-primary)" }}
/>
</div>
<div className="p-3" style={{ backgroundColor: "var(--color-surface)" }}>
<HomeLoginForm />
</div>
</div>
<a href="/register" className="relative block">
<div className="inline-block overflow-hidden rounded-lg border" style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 22%, transparent)" }}>
<img src="/assets/images/EnterHubbly.png" alt="Register" className="block" />
</div>
<span
className="pointer-events-none absolute inset-0 flex items-center text-4xl font-extrabold text-white"
style={{ textShadow: "1px 1px 2px rgba(0,0,0,0.8)", marginLeft: "15px" }}
>
REGISTER NOW
</span>
</a>
{recentUsers.length > 0 && (
<div
className="overflow-hidden rounded-lg border bg-white dark:bg-gray-800"
style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 22%, transparent)" }}
>
<div
className="flex h-12 w-full items-center justify-center bg-repeat"
style={{
background:
"linear-gradient(140deg, #0A1F0A 0%, #0F2D0F 20%, #153C15 40%, #1B4A1B 60%, #205820 75%, #1A4A1A 90%, #143814 100%)",
}}
>
<h2 className="px-3 text-xl font-bold text-white">Recent Users</h2>
</div>
<div className="grid grid-cols-6 gap-4 p-4">
{recentUsers.map((u) => (
<div
key={u.username}
className="relative overflow-visible rounded-md bg-white p-0 shadow-lg dark:bg-gray-800"
style={{ width: "50px", height: "50px", justifySelf: "center" }}
>
<img
src={avatarImageUrl(imager, u.look, { headOnly: true, direction: 3 })}
alt={u.username}
loading="lazy"
decoding="async"
style={{
position: "absolute",
top: "-12px",
left: "50%",
transform: "translateX(-50%)",
width: "auto",
height: "auto",
maxWidth: "none",
maxHeight: "none",
}}
/>
</div>
))}
</div>
</div>
)}
</div>
<div className="flex w-full flex-col gap-4 md:w-2/3">
<div
className="overflow-hidden rounded-lg border bg-white dark:bg-gray-800"
style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 22%, transparent)" }}
>
<div
className="flex h-12 w-full items-center justify-center"
style={{
background:
"linear-gradient(140deg, #1a3a5c 0%, #1e4a6e 20%, #2a5a80 40%, #3a6a90 60%, #4a7aa0 75%, #3a6a90 90%, #2a5a80 100%)",
}}
>
<h2 className="text-xl font-bold text-white">{t("latestNews") || "Latest News"}</h2>
</div>
<div className="p-4">
{articles.length === 0 ? (
<p className="py-8 text-center text-gray-500 dark:text-gray-400">No articles available</p>
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{articles.map((a) => (
<Link
key={a.slug}
href={`/news/${a.slug}`}
className="group relative w-full overflow-hidden rounded-lg transition-all duration-200"
style={{
height: "215px",
backgroundColor: "var(--color-surface)",
border: "1px solid color-mix(in srgb, var(--color-text-muted) 15%, transparent)",
}}
>
<div className="relative h-full w-full overflow-hidden rounded-lg">
<img
src={a.image}
alt={a.title}
className="h-full w-full rounded-lg object-cover transition-all duration-300 group-hover:scale-105"
loading="lazy"
/>
<div
className="absolute left-0 w-full p-2"
style={{
bottom: "-2px",
background: "linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%)",
}}
>
<h3 className="truncate text-lg font-semibold text-white text-shadow-sm">{a.title}</h3>
{a.shortStory && (
<p className="truncate text-xs font-normal text-white text-shadow-sm">
{excerpt(a.shortStory, 80)}
</p>
)}
</div>
</div>
</Link>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}