latest changes

This commit is contained in:
remco committed 2026-07-01 15:06:52 +02:00
1 parent 56cd6fd058
commit 1de72863db
40 files changed
+3837 -568

No files matched your search

-100
View File
@@ -1,100 +0,0 @@
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 { buildNitroClientUrl } from "@/lib/services/clients";
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 base URL may be stored under either settings key.
const [hotelNameRaw, flashUrl, flashUrlAlt] = await Promise.all([
siteSettings.get("hotel_name", "Atom").catch(() => "Atom"),
siteSettings.get("flash_client_url", "").catch(() => ""),
siteSettings.get("client_flash_url", "").catch(() => ""),
]);
const hotelName = hotelNameRaw ?? "Atom";
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, ip);
// Use a configured Flash base URL (with the SSO ticket appended); otherwise
// fall back to the Nitro client so the launcher still works.
const flashBase = flashUrl || flashUrlAlt || "";
const flashSrc = flashBase
? `${flashBase}${flashBase.includes("?") ? "&" : "?"}sso=${encodeURIComponent(ticket)}`
: "";
const src = flashSrc || (await buildNitroClientUrl(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>
);
}
+100 -50
View File
@@ -1,61 +1,111 @@
"use client";
import { useEffect, useState } from "react";
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 { buildNitroClientUrl } from "@/lib/services/clients";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
export default function ClientPage() {
const [ticket, setTicket] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hotelName, setHotelName] = useState("Hubbly");
const [launched, setLaunched] = useState(false);
export default async function ClientPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
useEffect(() => {
async function fetchTicket() {
try {
const res = await fetch("/api/client/sso");
if (!res.ok) throw new Error("Failed to get SSO ticket");
const data = await res.json();
setTicket(data.ticket);
setHotelName(data.hotelName);
} catch (e) {
setError("Could not generate login ticket");
} finally {
setLoading(false);
}
}
fetchTicket();
}, []);
const userId = Number(session.user.id);
const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
useEffect(() => {
if (ticket && !launched) {
setLaunched(true);
const clientUrl = `/nitro-client/?sso=${encodeURIComponent(ticket)}`;
const fullUrl = `${window.location.origin}${clientUrl}`;
window.open(fullUrl, "_blank", "noopener,noreferrer");
}
}, [ticket, launched]);
const hdrs = await headers();
const ip =
hdrs.get("x-forwarded-for")?.split(",")[0]?.trim() ?? hdrs.get("x-real-ip") ?? "0.0.0.0";
if (loading) {
return (
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
<header
className="flex h-14 items-center justify-between border-b px-4"
style={{ backgroundColor: "var(--color-navbar)", borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" }}
>
<div className="flex items-center gap-4">
<span className="font-bold text-lg">{hotelName}</span>
<span className="text-sm text-gray-500">Loading client…</span>
</div>
</header>
<main className="flex-1 flex items-center justify-center">
<ContentCard icon="⏳" title="Loading" subtitle="Generating SSO ticket…" />
</main>
</div>
);
}
// Faithful to AtomCMS's NitroController: issue (and persist) the SSO ticket on
// the launcher render, then hand it to the Nitro client at {nitro_path}.
const ticket = await issueSsoTicket(prisma, userId, hotelName, ip);
const src = await buildNitroClientUrl(ticket);
if (error || !ticket) {
return (
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
<header
className="flex h-14 items-center justify-between border-b px-4"
style={{ backgroundColor: "var(--color-navbar)", borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" }}
>
<div className="flex items-center gap-4">
<span className="font-bold text-lg">{hotelName}</span>
</div>
</header>
<main className="flex-1 flex items-center justify-center">
<ContentCard icon="⚠" title="Error" subtitle={error ?? "No SSO ticket"}>
<EmptyState icon="⚠">{error ?? "Could not generate SSO ticket"}</EmptyState>
</ContentCard>
</main>
</div>
);
}
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard
icon="🏨"
title={`Launching ${hotelName}…`}
subtitle="Your single sign-on ticket is ready"
action={
<Link href="/client/flash" className="btn btn-outline">
Use Flash client
</Link>
}
/>
{src ? (
<ContentCard padded={false}>
<iframe
title="Hotel client"
src={src}
style={{ width: "100%", height: "85vh", border: 0, borderRadius: 10, display: "block" }}
/>
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
<header
className="flex h-14 items-center justify-between border-b px-4"
style={{
backgroundColor: "var(--color-navbar)",
borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)",
color: "var(--color-navbar-text)",
}}
>
<div className="flex items-center gap-4">
<span className="font-bold text-lg">{hotelName}</span>
<span className="text-sm" style={{ color: "var(--color-navbar-text)" }}>Connected</span>
</div>
</header>
<main className="flex-1 flex items-center justify-center" style={{ backgroundColor: "var(--color-background)" }}>
<ContentCard>
<div style={{ textAlign: "center" }}>
<h1 style={{ fontSize: "1.75rem", fontWeight: 700, marginBottom: "0.5rem" }}>
Welcome to {hotelName}
</h1>
<p style={{ color: "var(--color-text-muted)", marginBottom: "2rem" }}>
Opening the client in a new tab… If it doesn't open,{" "}
<a href={`/nitro-client/?sso=${encodeURIComponent(ticket)}`} target="_blank" rel="noopener noreferrer">
click here
</a>.
</p>
</div>
</ContentCard>
) : (
<ContentCard icon="🔌" title="No client configured" subtitle="Set nitro_path in settings">
<EmptyState icon="🔌">
No Nitro client configured — set <code>nitro_path</code> (e.g.{" "}
<code>/Nitro-V3/dist</code>) in the admin settings. SSO ticket issued for this session:{" "}
<code>{ticket}</code>
</EmptyState>
</ContentCard>
)}
</main>
</main>
</div>
);
}
}