diff --git a/backend/src/auth.js b/backend/src/auth.js
index 37ef96c..d3c950b 100644
--- a/backend/src/auth.js
+++ b/backend/src/auth.js
@@ -27,9 +27,31 @@ export async function requireAuth(request, reply) {
}
}
+ const prefix = `${APP_SLUG}:`
+ let caps
+ if (Array.isArray(payload.caps)) {
+ caps = payload.caps.filter(c => c.startsWith(prefix)).map(c => c.slice(prefix.length))
+ } else {
+ // Legacy token: grant planner access, withhold settings
+ caps = ['planner']
+ }
+
request.user = {
email: payload.sub,
name: payload.name,
is_admin: payload.is_admin ?? false,
+ caps,
+ }
+}
+
+export function hasCap(request, cap) {
+ return request.user?.is_admin === true || request.user?.caps?.includes(cap) === true
+}
+
+export function requireCap(cap) {
+ return async (request, reply) => {
+ if (!hasCap(request, cap)) {
+ return reply.status(403).send({ error: `Missing capability: ${cap}` })
+ }
}
}
diff --git a/backend/src/routes/config.js b/backend/src/routes/config.js
index 7d4a310..e2672f5 100644
--- a/backend/src/routes/config.js
+++ b/backend/src/routes/config.js
@@ -1,4 +1,4 @@
-import { requireAuth } from '../auth.js'
+import { requireAuth, requireCap } from '../auth.js'
import { getConfig, setConfig } from '../db.js'
import { fetchSitesList, testConnection } from '../lib/newbook.js'
@@ -121,10 +121,9 @@ export async function configRoutes(app) {
return { ok: true, date }
})
- // ── GET /api/categories — admin: fetch from Newbook + saved order ─────────
+ // ── GET /api/categories — settings cap required ──────────────────────────
- app.get('/api/categories', async (req, reply) => {
- if (!req.user.is_admin) return reply.status(403).send({ error: 'Admin only' })
+ app.get('/api/categories', { preHandler: requireCap('settings') }, async (req, reply) => {
let sites
try {
@@ -160,10 +159,9 @@ export async function configRoutes(app) {
return { categories: ordered }
})
- // ── PUT /api/categories — admin: save order + exclusions ─────────────────
+ // ── PUT /api/categories — settings cap required ──────────────────────────
- app.put('/api/categories', async (req, reply) => {
- if (!req.user.is_admin) return reply.status(403).send({ error: 'Admin only' })
+ app.put('/api/categories', { preHandler: requireCap('settings') }, async (req, reply) => {
const { order, excluded } = req.body || {}
if (!Array.isArray(order)) return reply.status(400).send({ error: 'Invalid data' })
@@ -179,10 +177,9 @@ export async function configRoutes(app) {
return { ok: true }
})
- // ── POST /api/newbook/test — admin ────────────────────────────────────────
+ // ── POST /api/newbook/test — settings cap required ───────────────────────
- app.post('/api/newbook/test', async (req, reply) => {
- if (!req.user.is_admin) return reply.status(403).send({ error: 'Admin only' })
+ app.post('/api/newbook/test', { preHandler: requireCap('settings') }, async (_req, _reply) => {
return testConnection()
})
}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1e7000c..b5ec171 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -3,6 +3,7 @@ import { AuthGate } from './components/AuthGate'
import { Layout } from './components/Layout'
import { Planner } from './pages/Planner'
import { CategorySettings } from './pages/CategorySettings'
+import { can } from './types'
import type { User } from './types'
function AppRoutes({ user }: { user: User }) {
@@ -11,7 +12,7 @@ function AppRoutes({ user }: { user: User }) {