Role-based access control: guards on all critical APIs + redirects

INTEGRATION AUDIT — Fixed all gaps:

1. LOGIN REDIRECT
   - Community leaders → /dashboard/community (not /dashboard)
   - Fetches session after login to check role before redirect
   - Auth0 callback still goes to /dashboard (handled by #2)

2. DASHBOARD HOME REDIRECT
   - If role === community_leader or volunteer → router.replace(/community)
   - Prevents them from seeing the admin home page

3. API ROLE GUARDS (server-side)
   New: src/lib/roles.ts — permission matrix:
   - settings.write: super_admin, org_admin only
   - pledges.write: super_admin, org_admin only (status changes)
   - events.create: super_admin, org_admin only
   - imports.upload: super_admin, org_admin only (bank statements)
   - links.create: super_admin, org_admin, community_leader (they can create)
   - pledges.read: everyone except volunteer
   - dashboard.read: everyone except volunteer

   New: requirePermission() in session.ts
   Applied to:
   - PATCH /api/settings → settings.write
   - PUT /api/settings → settings.write
   - PATCH /api/pledges/[id] → pledges.write
   - POST /api/events → events.create
   - POST /api/imports/bank-statement → imports.upload

   Community leader attempting these gets 403 'Admin access required'

4. LAYOUT NAV (already done in previous commit)
   - community_leader sees: My Community, Share Links, Reports
   - No Money, No Settings, No 'New Appeal' button

WHAT COMMUNITY LEADER CAN DO:
✓ View /dashboard/community (their scoped dashboard)
✓ View /dashboard/collect (share links — they can create new links)
✓ View /dashboard/reports (financial summary)
✓ Create QR sources / links (POST /api/events/[id]/qr)
✓ Read pledges and dashboard data

WHAT COMMUNITY LEADER CANNOT DO:
✗ Change pledge statuses (PATCH /api/pledges/[id] → 403)
✗ Change settings (PATCH/PUT /api/settings → 403)
✗ Create appeals (POST /api/events → 403)
✗ Upload bank statements (POST /api/imports/bank-statement → 403)
✗ Manage team (POST/PATCH/DELETE /api/team → 403, already guarded)
✗ See /dashboard/money, /dashboard/settings (not in nav, home redirects)
This commit is contained in:
2026-03-04 21:58:25 +08:00
parent b771858280
commit b477dc30d1
9 changed files with 144 additions and 54 deletions

View File

@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from "react"
import { useRouter } from "next/navigation"
import { useSession } from "next-auth/react"
import { formatPence } from "@/lib/utils"
import { ArrowRight, Loader2, MessageCircle, Copy, Check, Share2, Upload } from "lucide-react"
import Link from "next/link"
@@ -29,6 +30,9 @@ const STATUS_LABELS: Record<string, { label: string; color: string; bg: string }
export default function DashboardPage() {
const router = useRouter()
const { data: session } = useSession()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const userRole = (session?.user as any)?.role
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [data, setData] = useState<any>(null)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -56,6 +60,13 @@ export default function DashboardPage() {
setLoading(false)
}, [])
// Redirect community leaders to their scoped dashboard
useEffect(() => {
if (userRole === "community_leader" || userRole === "volunteer") {
router.replace("/dashboard/community")
}
}, [userRole, router])
useEffect(() => {
fetchAll()
const i = setInterval(fetchAll, 15000)