Split floats→safe_count, reports→cash_summary, add history cap

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 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-02 14:35:56 +00:00
parent be670f724d
commit 4754231f6f
7 changed files with 33 additions and 20 deletions

View file

@ -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 = {

View file

@ -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 = []

View file

@ -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'

View file

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