Close remaining web gaps: rich profile, login history, lightbox/slider, flash client, admin chatlog/DJ/chart/WYSIWYG, niche API
- Rich profile (/u/[username]): wallet (credits/duckets/diamonds), friends grid (messenger_friendships), and owned rooms sections. - Login history: new website_login_logs table (model + migration 0007), recorded on every successful sign-in (ip + user-agent), surfaced on a new /settings/sessions page (with failed-attempt list from failed_logins). - Photos lightbox + home article slider (client components, no Swiper dep). - /client/flash launcher (SSO ticket like the Nitro page). - Admin: private chatlogs section in /admin/logs, /admin/radio/moderation (shout moderation), a "users by rank" inline bar chart on the dashboard, and a TinyMCE rich-text editor on the article admin forms. - Niche API: /api/values/[id], /api/guilds(+/[id]), /api/radio/auto-play. Verified live (prod, amx_test): login recorded → /settings/sessions shows it with device; profile renders wallet/friends/rooms; dashboard chart + private-chat logs + /client/flash + /api/guilds all OK. Reverted test data. tsc 0, vitest 49/49, next build 0.
This commit is contained in:
1 parent
f7b3845131
commit
4dfe698009
25 files changed
+1463
-64
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import Link from "next/link";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
import { issueSsoTicket } from "@/lib/auth/sso-ticket";
|
||||
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 FlashClientPage() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) redirect("/login");
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
// The Flash client URL may be stored under either key depending on the
|
||||
// AtomCMS settings seed — try both, then fall back to the Nitro URL so the
|
||||
// launcher still issues a working SSO ticket.
|
||||
const [hotelName, flashUrl, flashUrlAlt, nitroUrl] = await Promise.all([
|
||||
siteSettings.get("hotel_name", "Atom").catch(() => "Atom"),
|
||||
siteSettings.get("flash_client_url", "").catch(() => ""),
|
||||
siteSettings.get("client_flash_url", "").catch(() => ""),
|
||||
siteSettings.get("nitro_client_url", "").catch(() => ""),
|
||||
]);
|
||||
|
||||
const clientUrl = flashUrl || flashUrlAlt || nitroUrl || "";
|
||||
|
||||
const hdrs = await headers();
|
||||
const ip =
|
||||
hdrs.get("x-forwarded-for")?.split(",")[0]?.trim() ?? hdrs.get("x-real-ip") ?? "0.0.0.0";
|
||||
|
||||
// Faithful to AtomCMS's client controllers: issue (and persist) the SSO ticket
|
||||
// on the launcher render, then hand it to the Flash client.
|
||||
const ticket = await issueSsoTicket(prisma, userId, hotelName ?? "Atom", ip);
|
||||
|
||||
const src = clientUrl
|
||||
? `${clientUrl}${clientUrl.includes("?") ? "&" : "?"}sso=${encodeURIComponent(ticket)}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard
|
||||
icon="🎮"
|
||||
title={`Launching ${hotelName ?? "Atom"} (Flash)…`}
|
||||
subtitle="Your single sign-on ticket is ready"
|
||||
action={
|
||||
<Link href="/client" className="btn btn-outline">
|
||||
Use Nitro client
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
{src ? (
|
||||
<>
|
||||
<ContentCard
|
||||
icon="🚀"
|
||||
title="Open the Flash client"
|
||||
subtitle="Flash works best in its own tab"
|
||||
action={
|
||||
<a
|
||||
href={src}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Launch in a new tab
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
<ContentCard padded={false}>
|
||||
<iframe
|
||||
title="Flash hotel client"
|
||||
src={src}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "80vh",
|
||||
border: 0,
|
||||
borderRadius: 10,
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</ContentCard>
|
||||
</>
|
||||
) : (
|
||||
<ContentCard
|
||||
icon="🔌"
|
||||
title="No Flash client configured"
|
||||
subtitle="Set flash_client_url in settings"
|
||||
>
|
||||
<EmptyState icon="🔌">
|
||||
No Flash client URL configured (set <code>flash_client_url</code> in settings). SSO
|
||||
ticket issued for this session: <code>{ticket}</code>
|
||||
</EmptyState>
|
||||
</ContentCard>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
@@ -36,6 +37,11 @@ export default async function ClientPage() {
|
||||
icon="🏨"
|
||||
title={`Launching ${hotelName ?? "Atom"}…`}
|
||||
subtitle="Your single sign-on ticket is ready"
|
||||
action={
|
||||
<Link href="/client/flash" className="btn btn-outline">
|
||||
Use Flash client
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
{src ? (
|
||||
|
||||
Reference in new issue
Block a user