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:
jtricerolph 2026-07-02 14:19:30 +00:00
parent 43c332be9b
commit be670f724d
13 changed files with 117 additions and 52 deletions

View file

@ -2,11 +2,12 @@ import { useState, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { api } from '../api'
import { PageHeader, Card, Btn, StatusBadge } from '../components/Layout'
import { fmtGBP, today } from '../types'
import type { CashUp } from '../types'
import { fmtGBP, today, can } from '../types'
import type { CashUp, User } from '../types'
export function History() {
export function History({ user }: { user: User }) {
const navigate = useNavigate()
const canFinalise = can(user, 'finalise')
const [rows, setRows] = useState<CashUp[]>([])
const [total, setTotal] = useState(0)
const [offset, setOffset] = useState(0)
@ -99,7 +100,7 @@ export function History() {
<input type="date" value={to} onChange={e => { setTo(e.target.value); setOffset(0) }} style={inpSt} />
</div>
<Btn onClick={() => { setOffset(0); load() }} small>Filter</Btn>
{selected.size > 0 && (
{canFinalise && selected.size > 0 && (
<Btn onClick={bulkFinalize} small>Finalise {selected.size} selected</Btn>
)}
</Card>
@ -129,7 +130,7 @@ export function History() {
{rows.map(row => (
<tr key={row.id} style={{ borderBottom: '1px solid var(--card-border)' }}>
<td style={tdSt}>
{row.status === 'draft' && (
{canFinalise && row.status === 'draft' && (
<input type="checkbox" checked={selected.has(row.id)}
onChange={() => toggleSelect(row.id)} />
)}
@ -151,7 +152,7 @@ export function History() {
onClick={() => navigate(`/daily?date=${row.session_date.slice(0, 10)}`)}>
{row.status === 'draft' ? 'Edit' : 'View'}
</Btn>
{row.status === 'draft' && (
{canFinalise && row.status === 'draft' && (
<Btn small variant="danger" onClick={() => deleteDraft(row.id)}>Delete</Btn>
)}
</td>