Enforce granular capabilities across cashup
Backend (server-side enforcement, not just UI): - auth.js: read caps from JWT; hasCap() + requireCap() helpers; legacy-token fallback (full access minus settings) so existing sessions keep working until re-login - finalise: submit final, delete draft, bulk-finalise, attachments - reports: multiday report, cash summary, debtors - floats: float management + safe count - settings: settings mutations (was is_admin) - count: draft save, newbook fetch Frontend: - can(user, cap) helper; User.caps from /verify - Nav items, routes and actions (Submit Final, delete, bulk-finalise) gated on capabilities; non-finalisers see a draft-only hint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
43c332be9b
commit
be670f724d
13 changed files with 117 additions and 52 deletions
|
|
@ -27,9 +27,39 @@ export async function requireAuth(request, reply) {
|
|||
}
|
||||
}
|
||||
|
||||
// Capabilities arrive as "<app>:<cap>" strings in the JWT. Store the bare
|
||||
// cap slugs for this app (e.g. "finalise") plus admin status.
|
||||
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 issued before granular capabilities existed. Reproduce the
|
||||
// old behaviour: full access except settings (which was is_admin-gated).
|
||||
// These users get precise capabilities the next time they log in.
|
||||
caps = ['count', 'finalise', 'reports', 'floats']
|
||||
}
|
||||
|
||||
request.user = {
|
||||
email: payload.sub,
|
||||
name: payload.name,
|
||||
is_admin: payload.is_admin ?? false,
|
||||
caps,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the authenticated user holds the given capability.
|
||||
// Admins implicitly hold every capability.
|
||||
export function hasCap(request, cap) {
|
||||
return request.user?.is_admin === true || request.user?.caps?.includes(cap) === true
|
||||
}
|
||||
|
||||
// Fastify preHandler factory — reject the request unless the user holds `cap`.
|
||||
// Use after requireAuth: { preHandler: [requireAuth, requireCap('finalise')] }
|
||||
export function requireCap(cap) {
|
||||
return async (request, reply) => {
|
||||
if (!hasCap(request, cap)) {
|
||||
return reply.status(403).send({ error: `Missing capability: ${cap}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue