Files
Epicure/apps/web/lib/require-admin-page.ts
T
Arnaud 4c3880e07f feat: moderator-scoped admin access + fix push notifications not displaying (v0.66.0)
Moderator role existed in the schema and was already respected by
comment deletion, but every admin page/route treated moderator
identically to a regular user (403/redirect). Wires it up narrowly:
admin/layout.tsx now lets admin+moderator through and filters the
nav by role, while every admin-only page (users, tiers, settings,
webhooks, insights, etc.) explicitly redirects moderators away via a
new requireFullAdminPage() helper -- the nav filter is UX, this is
the actual gate. Moderators land on Reports and Recipes: reports
GET/PATCH now accept requireAdmin({allowModerator: true}), and a new
PATCH /api/v1/admin/recipes/[id] lets admin+moderator unpublish a
public recipe (flip to private) as a takedown action, audit-logged.

Also found and fixed a real bug while auditing the PWA push pipeline
for a "push click-through" gap: public/sw.js had no `push` event
listener at all, so incoming push messages never displayed anything
-- push was silently non-functional end-to-end despite the
subscribe/send plumbing all working. Added the push listener
(showNotification) and a notificationclick listener that focuses an
existing tab or opens one at the payload's url.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 23:31:47 +02:00

30 lines
1.4 KiB
TypeScript

import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
import { db, users, eq } from "@epicure/db";
export type StaffRole = "admin" | "moderator";
/** Server-component equivalent of requireAdmin/lib/api-auth.ts — used by
* admin/layout.tsx to gate entry to the whole /admin tree (admin AND
* moderator both pass) and by individual page components that need to
* additionally restrict themselves to admin only. Always re-queries the
* role fresh, same reasoning as requireAdmin: session.user.role comes from
* a 5-minute cookieCache. */
export async function getStaffRole(): Promise<StaffRole | null> {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return null;
const [dbUser] = await db.select({ role: users.role }).from(users).where(eq(users.id, session.user.id)).limit(1);
if (dbUser?.role === "admin" || dbUser?.role === "moderator") return dbUser.role;
return null;
}
/** Redirects moderators to /admin/reports (their only allowed landing area)
* and non-staff to /recipes. Call at the top of any admin page that should
* stay admin-only. */
export async function requireFullAdminPage(): Promise<void> {
const role = await getStaffRole();
if (role === "moderator") redirect("/admin/reports");
if (role !== "admin") redirect("/recipes");
}