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

@ -3,7 +3,7 @@ import { RefreshCw, Save, CheckCircle, Loader, Camera, FileText, X } from 'lucid
import { api, uploadAttachment } from '../api'
import { PageHeader, Card, Btn, StatusBadge } from '../components/Layout'
import {
GBP_DENOMINATIONS, fmtGBP, today,
GBP_DENOMINATIONS, fmtGBP, today, can,
type User, type CashUp, type Denomination, type CardMachine,
type PaymentTotals, type ReconciliationRow, type TillPayment, type Attachment,
type TransactionBreakdown, type TransactionItem,
@ -35,7 +35,8 @@ interface Props { user: User }
// flow: checking → empty (no record) | editing (draft) | locked (final)
type PageState = 'checking' | 'empty' | 'editing' | 'locked'
export function DailyCashUp({ user: _user }: Props) {
export function DailyCashUp({ user }: Props) {
const canFinalise = can(user, 'finalise')
const [date, setDate] = useState(today())
const [pageState, setPageState] = useState<PageState>('checking')
const [cashUp, setCashUp] = useState<CashUp | null>(null)
@ -465,15 +466,21 @@ export function DailyCashUp({ user: _user }: Props) {
{/* Action buttons */}
{pageState === 'editing' && (
<div style={{ display: 'flex', gap: '0.75rem' }}>
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>
<Btn onClick={() => save('draft')} disabled={saving} variant="secondary">
<Save size={14} style={{ marginRight: '0.4rem' }} />
{saving ? 'Saving…' : 'Save Draft'}
</Btn>
<Btn onClick={() => save('final')} disabled={saving}>
<CheckCircle size={14} style={{ marginRight: '0.4rem' }} />
{saving ? 'Submitting…' : 'Submit Final'}
</Btn>
{canFinalise ? (
<Btn onClick={() => save('final')} disabled={saving}>
<CheckCircle size={14} style={{ marginRight: '0.4rem' }} />
{saving ? 'Submitting…' : 'Submit Final'}
</Btn>
) : (
<span style={{ fontSize: '0.8rem', color: 'var(--text-mid)' }}>
Save as draft a manager with finalise permission will submit it.
</span>
)}
</div>
)}