feat(app): Next.js 16 + Auth.js v5 portal skeleton
ci / shared (push) Successful in 4s
ci / test (push) Successful in 26s
ci / e2e (push) Has been skipped
ci / image (push) Has been skipped

Next.js 16 + Auth.js v5 skeleton: host→slug middleware, tenant-context layout, OIDC sign-in flow against breakpilot-dev realm. 100% coverage on src/lib. Bumps next to 16.2.6 to clear trivy CVEs in 15.0.3.
This commit was merged in pull request #4.
This commit is contained in:
2026-05-19 09:35:05 +00:00
parent 3c7409ee9e
commit e7a1290246
25 changed files with 5611 additions and 14 deletions
+44
View File
@@ -0,0 +1,44 @@
import { auth, signIn, signOut } from "@/auth";
export default async function Dashboard({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const session = await auth();
if (!session) {
async function login() {
"use server";
await signIn("keycloak", { redirectTo: `/${slug}/dashboard` });
}
return (
<div>
<h1>Sign in to {slug}</h1>
<form action={login}>
<button type="submit">Sign in with Keycloak</button>
</form>
</div>
);
}
async function logout() {
"use server";
await signOut({ redirectTo: `/${slug}/dashboard` });
}
return (
<div>
<h1>Dashboard</h1>
<p>
Welcome, {session.user?.name ?? session.user?.email ?? "user"}. This is the{" "}
<code>{slug}</code> dashboard. Real product tiles, settings, billing land
in M5.2 / M10.1.
</p>
<form action={logout} style={{ marginTop: 24 }}>
<button type="submit">Sign out</button>
</form>
</div>
);
}
+36
View File
@@ -0,0 +1,36 @@
import { notFound } from "next/navigation";
import type { ReactNode } from "react";
import { fetchTenantBySlug } from "@/lib/tenant-registry";
export default async function TenantLayout({
children,
params,
}: {
children: ReactNode;
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const tenant = await fetchTenantBySlug(slug);
if (!tenant) notFound();
return (
<div>
<header
style={{
padding: "12px 24px",
borderBottom: "1px solid #eaeaea",
background: "white",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<strong>{tenant.name}</strong>
<span style={{ fontSize: 12, color: "#666" }}>
{tenant.plan} · {tenant.status}
</span>
</header>
<main style={{ padding: 24 }}>{children}</main>
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { redirect } from "next/navigation";
export default async function TenantRoot({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
redirect(`/${slug}/dashboard`);
}