Add CalendarClock icon, planner/settings capabilities

- backend/src/auth.js: extract caps from JWT (planner/settings), hasCap/requireCap
- backend/src/routes/config.js: gate categories + test-connection on requireCap('settings')
- frontend/src/types.ts: add caps[] to User, export can() helper
- frontend/src/App.tsx: gate /settings route on can(user, 'settings')
- frontend/src/components/Layout.tsx: gate nav item on can(user, 'settings')

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-02 14:29:58 +00:00
parent 773d323060
commit 135f1f6cf0
6 changed files with 39 additions and 13 deletions

View file

@ -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}` })
}
}
}

View file

@ -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()
})
}