fix(admin): mount Clerk client SDK on admin panel to stop UNAUTHORIZED

The admin panel (src/app/p/[secret]/layout.tsx) never wrapped its tree
in ClerkThemeProvider, unlike (app)/layout.tsx — a SpeedUp-Plan P0
optimization that deliberately kept Clerk's client bundle off routes
without a <UserButton/>. Consequence: no client-side session-token
refresh ran on admin pages. The initial page load stayed authenticated
(Clerk's server-side handshake reissues the token on full document
navigations), but subsequent tRPC XHRs — e.g. legal.publish — started
failing with UNAUTHORIZED/401 a few minutes in, since that handshake
only fires for navigations, not fetch calls. Confirmed live: a fresh
page reload always restored a working session, matching this exactly.

Mounting ClerkThemeProvider in the admin layout re-adds background
token refresh. Also gives the legal-publish error handler an
actionable message for UNAUTHORIZED specifically (distinct from other
failures), since a residual case can still occur after a real 7-day
session expiry.
This commit is contained in:
admin committed 2026-09-04 17:56:43 +02:00
1 parent e9378d9d7a
commit c225c9aef7
2 files changed
+48 -24

No files matched your search

+36 -23
View File
@@ -5,6 +5,7 @@ import { createTRPCCaller } from "@/trpc/server";
import { timingSafeStringEqual } from "@/lib/timing-safe-compare";
import { AdminMobileNav } from "@/components/admin/AdminMobileNav";
import { AdminSidebarNav } from "@/components/admin/AdminSidebarNav";
import { ClerkThemeProvider } from "@/components/providers/ClerkThemeProvider";
import {
LayoutDashboard,
Users,
@@ -80,31 +81,43 @@ export default async function AdminLayout({ children, params }: AdminLayoutProps
}));
return (
// Admin stays permanently dark regardless of the visitor site's theme
// toggle — a moderator's ops tool, not a marketing surface — but now
// pinned to PawFeed's own dark palette (the `dark` class resolves the
// real design tokens) instead of a generic, disconnected zinc scaffold.
<div className="dark flex min-h-screen flex-col bg-background text-foreground md:flex-row">
{/* Mobile topbar + drawer — hidden md: and up */}
<AdminMobileNav nav={navForMobile} base={base} />
// ClerkThemeProvider mounts Clerk's client SDK, which keeps the short-lived
// __session cookie refreshed in the background. Without it (as before),
// this route tree never renewed the token client-side — the initial page
// load kept working (Clerk's server-side "handshake" reissues the token on
// full document navigations) but subsequent tRPC XHRs (e.g. legal.publish)
// started failing with UNAUTHORIZED/401 a few minutes into a session, since
// that handshake only runs for navigations, not fetch calls. This does
// re-add the ~240 KiB Clerk client bundle that (app)/layout.tsx's
// SpeedUp-Plan P0 deliberately kept off non-admin routes — acceptable here
// since the admin panel is an internal ops tool, not perf-critical.
<ClerkThemeProvider>
{/* Admin stays permanently dark regardless of the visitor site's theme
toggle — a moderator's ops tool, not a marketing surface — but now
pinned to PawFeed's own dark palette (the `dark` class resolves the
real design tokens) instead of a generic, disconnected zinc scaffold. */}
<div className="dark flex min-h-screen flex-col bg-background text-foreground md:flex-row">
{/* Mobile topbar + drawer — hidden md: and up */}
<AdminMobileNav nav={navForMobile} base={base} />
{/* Sidebar — hidden below md:, permanent from md: up */}
<aside className="hidden md:flex w-56 shrink-0 border-r border-border flex-col py-6 px-4 gap-1 h-screen sticky top-0 overflow-y-auto">
<AdminSidebarNav base={base} nav={navForMobile} />
{/* Sidebar — hidden below md:, permanent from md: up */}
<aside className="hidden md:flex w-56 shrink-0 border-r border-border flex-col py-6 px-4 gap-1 h-screen sticky top-0 overflow-y-auto">
<AdminSidebarNav base={base} nav={navForMobile} />
<div className="mt-auto pt-3 border-t border-border">
<Link
href="/feed"
className="flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
>
<ArrowLeft className="h-4 w-4 shrink-0" />
Zurück
</Link>
</div>
</aside>
<div className="mt-auto pt-3 border-t border-border">
<Link
href="/feed"
className="flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
>
<ArrowLeft className="h-4 w-4 shrink-0" />
Zurück
</Link>
</div>
</aside>
{/* Content */}
<main className="min-w-0 flex-1 overflow-auto p-4 md:p-8">{children}</main>
</div>
{/* Content */}
<main className="min-w-0 flex-1 overflow-auto p-4 md:p-8">{children}</main>
</div>
</ClerkThemeProvider>
);
}
+12 -1
View File
@@ -45,7 +45,18 @@ export default function AdminLegalPage() {
toast.success("Veröffentlicht — Owner sehen die Meldung beim nächsten Laden der App");
setChangeSummary("");
},
onError: () => toast.error("Veröffentlichung fehlgeschlagen"),
// UNAUTHORIZED means the Clerk session cookie went stale (see
// ClerkThemeProvider in layout.tsx for the background-refresh fix) —
// distinct from every other failure, so surface it with an actionable
// message instead of the generic one. The typed text is preserved
// (no setChangeSummary("") here) so a reload doesn't lose it.
onError: (error) => {
if (error.data?.code === "UNAUTHORIZED") {
toast.error("Session abgelaufen — bitte Seite neu laden und erneut versuchen");
} else {
toast.error("Veröffentlichung fehlgeschlagen");
}
},
})
);