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>
This commit is contained in:
@@ -117,3 +117,38 @@ async function replayPendingActions() {
|
||||
self.addEventListener("sync", event => {
|
||||
if (event.tag === SYNC_TAG) event.waitUntil(replayPendingActions());
|
||||
});
|
||||
|
||||
// --- Push: display incoming notifications and handle taps ---
|
||||
// lib/push.ts sends {title, body, url} as the payload. Without a "push"
|
||||
// listener, a push message reaches the browser but never displays anything
|
||||
// — showNotification() must be called explicitly.
|
||||
self.addEventListener("push", event => {
|
||||
let data = {};
|
||||
try {
|
||||
data = event.data ? event.data.json() : {};
|
||||
} catch {
|
||||
data = {};
|
||||
}
|
||||
const title = data.title || "Epicure";
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, {
|
||||
body: data.body || "",
|
||||
icon: "/icon-192.svg",
|
||||
badge: "/icon-192.svg",
|
||||
data: { url: data.url || "/" },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("notificationclick", event => {
|
||||
event.notification.close();
|
||||
const url = event.notification.data && event.notification.data.url ? event.notification.data.url : "/";
|
||||
event.waitUntil(
|
||||
self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(clientList => {
|
||||
const targetUrl = new URL(url, self.location.origin).href;
|
||||
const existing = clientList.find(c => c.url === targetUrl);
|
||||
if (existing) return existing.focus();
|
||||
return self.clients.openWindow(url);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user