From c0916cd3a2188bb235e13a8c32be2b0bf190220c Mon Sep 17 00:00:00 2001 From: sazzadulalambd Date: Sat, 9 May 2026 12:51:28 +0600 Subject: [PATCH] feat: implement role-based access control for KYC workflows and add permissions documentation --- src/app/admin/kyc/[id]/page.tsx | 48 +++++++++++++++++++++++++++------ src/app/login/page.tsx | 24 +++++++++++++++++ src/components/Sidebar.tsx | 42 +++++++++++++++++++---------- src/lib/auth.ts | 37 +++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 22 deletions(-) diff --git a/src/app/admin/kyc/[id]/page.tsx b/src/app/admin/kyc/[id]/page.tsx index da49446..d111f3d 100644 --- a/src/app/admin/kyc/[id]/page.tsx +++ b/src/app/admin/kyc/[id]/page.tsx @@ -7,9 +7,10 @@ import { Shield, Check, Clock, Bike, User, Phone, MapPin, FileText, Image, DollarSign, Wrench, Battery, CheckCircle, XCircle, ArrowLeft, Save, Printer, Send, - MessageSquare, Edit, UserCheck, Wallet, Store, Globe, Calendar, Briefcase, Plus, Upload + MessageSquare, Edit, UserCheck, Wallet, Store, Globe, Calendar, Briefcase, Plus, Upload, Lock } from 'lucide-react'; import toast from 'react-hot-toast'; +import { hasPermission, canApproveKycDocument, canRejectKycDocument, canMakeValidUser } from '@/lib/auth'; type ApplicationSource = 'app' | 'web' | 'walkin' | 'referral'; type KYCType = 'biker' | 'investor' | 'swapstation' | 'merchant' | 'general'; @@ -266,6 +267,18 @@ export default function KYCDetailPage() { const [rejectReason, setRejectReason] = useState(''); const [uploadDocId, setUploadDocId] = useState(null); + const [permApprove, setPermApprove] = useState(false); + const [permReject, setPermReject] = useState(false); + const [permMakeValid, setPermMakeValid] = useState(false); + const [permDocUpload, setPermDocUpload] = useState(false); + + useEffect(() => { + setPermApprove(canApproveKycDocument()); + setPermReject(canRejectKycDocument()); + setPermMakeValid(canMakeValidUser()); + setPermDocUpload(hasPermission('kyc.doc_upload')); + }, []); + useEffect(() => { const found = mockRequests.find(r => r.id === id); if (found) { @@ -425,7 +438,7 @@ export default function KYCDetailPage() { <> {/* Top Row on Mobile: Make [Type] Button */}
- {request.type === 'biker' && request.status !== 'approved' && ( + {permMakeValid && request.type === 'biker' && request.status !== 'approved' && ( )} - {request.type === 'investor' && request.status !== 'approved' && ( + {permMakeValid && request.type === 'investor' && request.status !== 'approved' && ( )} - {request.type === 'swapstation' && request.status !== 'approved' && ( + {permMakeValid && request.type === 'swapstation' && request.status !== 'approved' && ( )} - {request.type === 'merchant' && request.status !== 'approved' && ( + {permMakeValid && request.type === 'merchant' && request.status !== 'approved' && ( )} + {!permMakeValid && ( +
+ Make Valid User +
+ )}
{/* Bottom Row on Mobile: Edit, Note, SMS */} @@ -663,15 +681,29 @@ export default function KYCDetailPage() {
{doc.status === 'pending' && ( - + <> + {permDocUpload ? ( + + ) : ( + + )} + )} {(doc.status === 'uploaded' || doc.status === 'approved') && doc.imageUrl && ( )} {doc.status === 'uploaded' && ( <> - - + {permApprove ? ( + + ) : ( + + )} + {permReject ? ( + + ) : ( + + )} )} {doc.status === 'approved' && Approved} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index d530fae..0290f07 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -35,6 +35,18 @@ export default function LoginPage() { sessionStorage.setItem('userRole', user.role); sessionStorage.setItem('userName', user.name); + const rolePerms: Record = { + super_admin: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view'], + admin_manager: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view'], + staff: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'dashboard.view'], + accountant: ['dashboard.view', 'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete'], + investor: ['dashboard.view', 'kyc.request', 'kyc.view'], + biker: ['dashboard.view', 'kyc.request', 'kyc.view', 'rentals.view', 'rentals.create'], + 'swap-station': ['dashboard.view', 'kyc.request', 'kyc.view'], + merchant: ['dashboard.view', 'kyc.request', 'kyc.view', 'merchants.view'], + }; + sessionStorage.setItem('userPermissions', JSON.stringify(rolePerms[user.role] || [])); + switch (user.role) { case 'super_admin': case 'admin_manager': @@ -76,6 +88,18 @@ export default function LoginPage() { sessionStorage.setItem('userRole', user.role); sessionStorage.setItem('userName', user.name); + const rolePerms: Record = { + super_admin: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view'], + admin_manager: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user', 'dashboard.view'], + staff: ['kyc.request', 'kyc.view', 'kyc.doc_upload', 'dashboard.view'], + accountant: ['dashboard.view', 'accounting.view', 'accounting.create', 'accounting.edit', 'accounting.delete'], + investor: ['dashboard.view', 'kyc.request', 'kyc.view'], + biker: ['dashboard.view', 'kyc.request', 'kyc.view', 'rentals.view', 'rentals.create'], + 'swap-station': ['dashboard.view', 'kyc.request', 'kyc.view'], + merchant: ['dashboard.view', 'kyc.request', 'kyc.view', 'merchants.view'], + }; + sessionStorage.setItem('userPermissions', JSON.stringify(rolePerms[user.role] || [])); + switch (user.role) { case 'super_admin': case 'admin_manager': diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 8b9c3cd..b786383 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Bike, Settings, @@ -24,6 +24,18 @@ import { Calculator, Wrench } from 'lucide-react'; +import { getUserName, getUserRole, logout } from '@/lib/auth'; + +const ROLE_LABELS: Record = { + super_admin: 'Super Admin', + admin_manager: 'Admin Manager', + staff: 'Front Desk', + accountant: 'Accountant', + investor: 'Investor', + biker: 'Biker', + 'swap-station': 'Swap Station', + merchant: 'Merchant', +}; const adminNavItems = [ { label: 'Dashboard', href: '/admin', icon: BarChart3 }, @@ -67,11 +79,20 @@ export default function Sidebar() { const pathname = usePathname(); const [mobileOpen, setMobileOpen] = useState(false); const [expandedMenu, setExpandedMenu] = useState(null); + const [userName, setUserName] = useState('User'); + const [userRole, setUserRole] = useState('admin'); + + useEffect(() => { + setUserName(getUserName() || 'User'); + setUserRole(getUserRole() || 'staff'); + }, []); const isAdmin = pathname.startsWith('/admin'); const isInvestor = pathname.startsWith('/investor'); const isShop = pathname.startsWith('/shop'); + const roleLabel = ROLE_LABELS[userRole] || userRole; + const navItems = isAdmin ? adminNavItems : isInvestor ? investorNavItems : isShop ? shopNavItems : bikerNavItems; @@ -109,7 +130,7 @@ export default function Sidebar() {
- {isAdmin ? 'Admin' : isInvestor ? 'Investor' : isShop ? 'Shop' : 'Biker'} + {roleLabel}