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:
parent
be670f724d
commit
4754231f6f
7 changed files with 33 additions and 20 deletions
|
|
@ -37,7 +37,7 @@ export async function requireAuth(request, reply) {
|
||||||
// Legacy token issued before granular capabilities existed. Reproduce the
|
// Legacy token issued before granular capabilities existed. Reproduce the
|
||||||
// old behaviour: full access except settings (which was is_admin-gated).
|
// old behaviour: full access except settings (which was is_admin-gated).
|
||||||
// These users get precise capabilities the next time they log in.
|
// 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 = {
|
request.user = {
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,9 @@ export async function cashupRoutes(app) {
|
||||||
return { success, failed_count: failed.length }
|
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) => {
|
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 { status, from, to, offset = 0, limit = 20 } = req.query
|
||||||
const conditions = []
|
const conditions = []
|
||||||
const params = []
|
const params = []
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { pool } from '../db.js'
|
import { pool } from '../db.js'
|
||||||
import { requireAuth, requireCap } from '../auth.js'
|
import { requireAuth, hasCap } from '../auth.js'
|
||||||
|
|
||||||
export async function floatRoutes(app) {
|
export async function floatRoutes(app) {
|
||||||
app.addHook('preHandler', requireAuth)
|
app.addHook('preHandler', requireAuth)
|
||||||
app.addHook('preHandler', requireCap('floats'))
|
|
||||||
|
|
||||||
// POST /api/floats/save
|
// POST /api/floats/save
|
||||||
// body: { count_type, count_date, denominations, receipts?, total_counted, total_receipts?, target_amount?, variance?, notes }
|
// 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']
|
const validTypes = ['petty_cash', 'change_tin', 'safe_cash']
|
||||||
if (!validTypes.includes(count_type)) return reply.status(400).send({ error: 'invalid count_type' })
|
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' })
|
if (!count_date) return reply.status(400).send({ error: 'count_date required' })
|
||||||
|
|
||||||
const ins = await pool.query(
|
const ins = await pool.query(
|
||||||
|
|
@ -52,6 +54,9 @@ export async function floatRoutes(app) {
|
||||||
const validTypes = ['petty_cash', 'change_tin', 'safe_cash']
|
const validTypes = ['petty_cash', 'change_tin', 'safe_cash']
|
||||||
if (!validTypes.includes(type)) return reply.status(400).send({ error: 'invalid type' })
|
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(
|
const { rows } = await pool.query(
|
||||||
`SELECT * FROM float_counts WHERE count_type = $1 ORDER BY count_date DESC LIMIT $2 OFFSET $3`,
|
`SELECT * FROM float_counts WHERE count_type = $1 ORDER BY count_date DESC LIMIT $2 OFFSET $3`,
|
||||||
[type, parseInt(limit), parseInt(offset)]
|
[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' })
|
if (!rows.length) return reply.status(404).send({ error: 'Not found' })
|
||||||
|
|
||||||
const count = rows[0]
|
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([
|
const [denoms, receipts] = await Promise.all([
|
||||||
pool.query('SELECT * FROM float_denominations WHERE float_count_id = $1 ORDER BY denomination_value DESC', [count.id]),
|
pool.query('SELECT * FROM float_denominations WHERE float_count_id = $1 ORDER BY denomination_value DESC', [count.id]),
|
||||||
count.count_type === 'petty_cash'
|
count.count_type === 'petty_cash'
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,9 @@ function buildDateRange(startDate, numDays) {
|
||||||
|
|
||||||
export async function reportRoutes(app) {
|
export async function reportRoutes(app) {
|
||||||
app.addHook('preHandler', requireAuth)
|
app.addHook('preHandler', requireAuth)
|
||||||
app.addHook('preHandler', requireCap('reports'))
|
|
||||||
|
|
||||||
// POST /api/reports/multiday { start_date, num_days }
|
// 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 { start_date, num_days } = req.body
|
||||||
const days = parseInt(num_days)
|
const days = parseInt(num_days)
|
||||||
if (!start_date || isNaN(days) || days < 1 || days > 365) {
|
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 }
|
// 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 { start_date, num_days } = req.body
|
||||||
const days = parseInt(num_days)
|
const days = parseInt(num_days)
|
||||||
if (!start_date || isNaN(days) || days < 1 || days > 365) {
|
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
|
// 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
|
const { from, to } = req.query
|
||||||
if (!from || !to) return reply.status(400).send({ error: 'from and to required' })
|
if (!from || !to) return reply.status(400).send({ error: 'from and to required' })
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,13 @@ import { can, type User, type CashupCap } from './types'
|
||||||
|
|
||||||
function AppRoutes({ user }: { user: User }) {
|
function AppRoutes({ user }: { user: User }) {
|
||||||
// Landing route: first section the user can actually reach.
|
// 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.
|
// Redirect to home if the user lacks the capability for a route.
|
||||||
const guard = (cap: CashupCap, el: React.ReactNode) =>
|
const guard = (cap: CashupCap, el: React.ReactNode) =>
|
||||||
|
|
@ -22,13 +28,13 @@ function AppRoutes({ user }: { user: User }) {
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to={home} replace />} />
|
<Route path="/" element={<Navigate to={home} replace />} />
|
||||||
<Route path="/daily" element={guard('count', <DailyCashUp user={user} />)} />
|
<Route path="/daily" element={guard('count', <DailyCashUp user={user} />)} />
|
||||||
<Route path="/history" element={<History user={user} />} />
|
<Route path="/history" element={guard('history', <History user={user} />)} />
|
||||||
<Route path="/report" element={guard('reports', <MultiDayReport />)} />
|
<Route path="/report" element={guard('reports', <MultiDayReport />)} />
|
||||||
<Route path="/floats/*" element={guard('floats', <FloatManagement />)} />
|
<Route path="/floats/*" element={guard('floats', <FloatManagement />)} />
|
||||||
<Route path="/safe/*" element={guard('floats', <SafeCount />)} />
|
<Route path="/safe/*" element={guard('safe_count', <SafeCount />)} />
|
||||||
<Route path="/summary" element={guard('reports', <CashSummary />)} />
|
<Route path="/summary" element={guard('cash_summary', <CashSummary />)} />
|
||||||
<Route path="/settings" element={guard('settings', <SettingsPage user={user} />)} />
|
<Route path="/settings" element={guard('settings', <SettingsPage user={user} />)} />
|
||||||
<Route path="*" element={<Navigate to={home} replace />} />
|
<Route path="*" element={<Navigate to={home} replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ interface Props {
|
||||||
// `cap` gates the nav item's visibility; undefined = always shown (app access is enough).
|
// `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 }[] = [
|
const navItems: { to: string; label: string; icon: typeof Banknote; cap?: CashupCap }[] = [
|
||||||
{ to: '/daily', label: 'Daily Cash Up', icon: Banknote, cap: 'count' },
|
{ 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: '/report', label: 'Weekly Report', icon: BarChart2, cap: 'reports' },
|
||||||
{ to: '/floats', label: 'Float Management', icon: Wallet, cap: 'floats' },
|
{ to: '/floats', label: 'Float Management', icon: Wallet, cap: 'floats' },
|
||||||
{ to: '/safe', label: 'Safe Count', icon: Vault, cap: 'floats' },
|
{ to: '/safe', label: 'Safe Count', icon: Vault, cap: 'safe_count' },
|
||||||
{ to: '/summary', label: 'Cash Summary', icon: FileText, cap: 'reports' },
|
{ to: '/summary', label: 'Cash Summary', icon: FileText, cap: 'cash_summary' },
|
||||||
{ to: '/settings',label: 'Settings', icon: Settings, cap: 'settings' },
|
{ to: '/settings',label: 'Settings', icon: Settings, cap: 'settings' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
export interface User {
|
||||||
email: string
|
email: string
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue