From 4754231f6f7be34d46560f55721cbf1100ae9747 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 2 Jul 2026 14:35:56 +0000 Subject: [PATCH] =?UTF-8?q?Split=20floats=E2=86=92safe=5Fcount,=20reports?= =?UTF-8?q?=E2=86=92cash=5Fsummary,=20add=20history=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three new granular capabilities: - history: gates /history page and GET /api/cashup/history - cash_summary: gates /summary page and GET /api/reports/cash-summary - safe_count: gates /safe page and safe_cash float routes Updates legacy-token fallback to include all seven non-settings caps. Route guards and nav items updated to use the split caps. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/auth.js | 2 +- backend/src/routes/cashup.js | 3 ++- backend/src/routes/floats.js | 11 +++++++++-- backend/src/routes/reports.js | 7 +++---- frontend/src/App.tsx | 22 ++++++++++++++-------- frontend/src/components/Layout.tsx | 6 +++--- frontend/src/types.ts | 2 +- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/backend/src/auth.js b/backend/src/auth.js index 2efc5a1..ee3d17e 100644 --- a/backend/src/auth.js +++ b/backend/src/auth.js @@ -37,7 +37,7 @@ export async function requireAuth(request, reply) { // 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'] + caps = ['count', 'finalise', 'history', 'reports', 'cash_summary', 'floats', 'safe_count'] } request.user = { diff --git a/backend/src/routes/cashup.js b/backend/src/routes/cashup.js index 41f5262..9aac7c3 100644 --- a/backend/src/routes/cashup.js +++ b/backend/src/routes/cashup.js @@ -136,8 +136,9 @@ export async function cashupRoutes(app) { return { success, failed_count: failed.length } }) - // GET /api/cashup/history + // GET /api/cashup/history (requires cashup:history) app.get('/api/cashup/history', async (req, reply) => { + if (!hasCap(req, 'history')) return reply.status(403).send({ error: 'Missing capability: history' }) const { status, from, to, offset = 0, limit = 20 } = req.query const conditions = [] const params = [] diff --git a/backend/src/routes/floats.js b/backend/src/routes/floats.js index c26b56c..d62f719 100644 --- a/backend/src/routes/floats.js +++ b/backend/src/routes/floats.js @@ -1,9 +1,8 @@ import { pool } from '../db.js' -import { requireAuth, requireCap } from '../auth.js' +import { requireAuth, hasCap } from '../auth.js' export async function floatRoutes(app) { app.addHook('preHandler', requireAuth) - app.addHook('preHandler', requireCap('floats')) // POST /api/floats/save // body: { count_type, count_date, denominations, receipts?, total_counted, total_receipts?, target_amount?, variance?, notes } @@ -15,6 +14,9 @@ export async function floatRoutes(app) { const validTypes = ['petty_cash', 'change_tin', 'safe_cash'] if (!validTypes.includes(count_type)) return reply.status(400).send({ error: 'invalid count_type' }) + + const needsCap = count_type === 'safe_cash' ? 'safe_count' : 'floats' + if (!hasCap(req, needsCap)) return reply.status(403).send({ error: `Missing capability: ${needsCap}` }) if (!count_date) return reply.status(400).send({ error: 'count_date required' }) const ins = await pool.query( @@ -52,6 +54,9 @@ export async function floatRoutes(app) { const validTypes = ['petty_cash', 'change_tin', 'safe_cash'] if (!validTypes.includes(type)) return reply.status(400).send({ error: 'invalid type' }) + const needsCap = type === 'safe_cash' ? 'safe_count' : 'floats' + if (!hasCap(req, needsCap)) return reply.status(403).send({ error: `Missing capability: ${needsCap}` }) + const { rows } = await pool.query( `SELECT * FROM float_counts WHERE count_type = $1 ORDER BY count_date DESC LIMIT $2 OFFSET $3`, [type, parseInt(limit), parseInt(offset)] @@ -70,6 +75,8 @@ export async function floatRoutes(app) { if (!rows.length) return reply.status(404).send({ error: 'Not found' }) const count = rows[0] + const needsCap = count.count_type === 'safe_cash' ? 'safe_count' : 'floats' + if (!hasCap(req, needsCap)) return reply.status(403).send({ error: `Missing capability: ${needsCap}` }) const [denoms, receipts] = await Promise.all([ pool.query('SELECT * FROM float_denominations WHERE float_count_id = $1 ORDER BY denomination_value DESC', [count.id]), count.count_type === 'petty_cash' diff --git a/backend/src/routes/reports.js b/backend/src/routes/reports.js index 602b861..3319c0d 100644 --- a/backend/src/routes/reports.js +++ b/backend/src/routes/reports.js @@ -25,10 +25,9 @@ function buildDateRange(startDate, numDays) { export async function reportRoutes(app) { app.addHook('preHandler', requireAuth) - app.addHook('preHandler', requireCap('reports')) // POST /api/reports/multiday { start_date, num_days } - app.post('/api/reports/multiday', async (req, reply) => { + app.post('/api/reports/multiday', { preHandler: requireCap('reports') }, async (req, reply) => { const { start_date, num_days } = req.body const days = parseInt(num_days) if (!start_date || isNaN(days) || days < 1 || days > 365) { @@ -186,7 +185,7 @@ export async function reportRoutes(app) { }) // POST /api/reports/debtors-creditors { start_date, num_days } - app.post('/api/reports/debtors-creditors', async (req, reply) => { + app.post('/api/reports/debtors-creditors', { preHandler: requireCap('reports') }, async (req, reply) => { const { start_date, num_days } = req.body const days = parseInt(num_days) if (!start_date || isNaN(days) || days < 1 || days > 365) { @@ -206,7 +205,7 @@ export async function reportRoutes(app) { }) // GET /api/reports/cash-summary?from=YYYY-MM-DD&to=YYYY-MM-DD - app.get('/api/reports/cash-summary', async (req, reply) => { + app.get('/api/reports/cash-summary', { preHandler: requireCap('cash_summary') }, async (req, reply) => { const { from, to } = req.query if (!from || !to) return reply.status(400).send({ error: 'from and to required' }) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d111b0d..df59e08 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,7 +12,13 @@ import { can, type User, type CashupCap } from './types' function AppRoutes({ user }: { user: User }) { // Landing route: first section the user can actually reach. - const home = can(user, 'count') ? '/daily' : '/history' + const home = + can(user, 'count') ? '/daily' : + can(user, 'history') ? '/history' : + can(user, 'reports') ? '/report' : + can(user, 'cash_summary') ? '/summary' : + can(user, 'floats') ? '/floats' : + can(user, 'safe_count') ? '/safe' : '/daily' // Redirect to home if the user lacks the capability for a route. const guard = (cap: CashupCap, el: React.ReactNode) => @@ -22,13 +28,13 @@ function AppRoutes({ user }: { user: User }) { } /> - )} /> - } /> - )} /> - )} /> - )} /> - )} /> - )} /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> + )} /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 5ee9b75..7c97be3 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -12,11 +12,11 @@ interface Props { // `cap` gates the nav item's visibility; undefined = always shown (app access is enough). const navItems: { to: string; label: string; icon: typeof Banknote; cap?: CashupCap }[] = [ { to: '/daily', label: 'Daily Cash Up', icon: Banknote, cap: 'count' }, - { to: '/history', label: 'History', icon: ClipboardList }, + { to: '/history', label: 'History', icon: ClipboardList, cap: 'history' }, { to: '/report', label: 'Weekly Report', icon: BarChart2, cap: 'reports' }, { to: '/floats', label: 'Float Management', icon: Wallet, cap: 'floats' }, - { to: '/safe', label: 'Safe Count', icon: Vault, cap: 'floats' }, - { to: '/summary', label: 'Cash Summary', icon: FileText, cap: 'reports' }, + { to: '/safe', label: 'Safe Count', icon: Vault, cap: 'safe_count' }, + { to: '/summary', label: 'Cash Summary', icon: FileText, cap: 'cash_summary' }, { to: '/settings',label: 'Settings', icon: Settings, cap: 'settings' }, ] diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d41a728..939b51e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,4 +1,4 @@ -export type CashupCap = 'count' | 'finalise' | 'reports' | 'floats' | 'settings' +export type CashupCap = 'count' | 'finalise' | 'history' | 'reports' | 'cash_summary' | 'floats' | 'safe_count' | 'settings' export interface User { email: string