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,21 +2,22 @@ import { NavLink, useNavigate } from 'react-router-dom'
import {
Banknote, ClipboardList, BarChart2, Wallet, Vault, FileText, Settings, LogOut,
} from 'lucide-react'
import type { User } from '../types'
import { can, type User, type CashupCap } from '../types'
interface Props {
user: User
children: React.ReactNode
}
const navItems = [
{ to: '/daily', label: 'Daily Cash Up', icon: Banknote },
// `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: '/report', label: 'Weekly Report', icon: BarChart2 },
{ to: '/floats', label: 'Float Management', icon: Wallet },
{ to: '/safe', label: 'Safe Count', icon: Vault },
{ to: '/summary', label: 'Cash Summary', icon: FileText },
{ to: '/settings',label: 'Settings', icon: Settings },
{ 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: '/settings',label: 'Settings', icon: Settings, cap: 'settings' },
]
export function Layout({ user, children }: Props) {
@ -45,7 +46,7 @@ export function Layout({ user, children }: Props) {
</div>
<div style={{ flex: 1, padding: '0.5rem 0', overflowY: 'auto' }}>
{navItems.map(({ to, label, icon: Icon }) => (
{navItems.filter(item => !item.cap || can(user, item.cap)).map(({ to, label, icon: Icon }) => (
<NavLink key={to} to={to} style={({ isActive }) => ({
display: 'flex', alignItems: 'center', gap: '0.625rem',
padding: '0.625rem 1rem', textDecoration: 'none',