Implemented rbac
This commit is contained in:
@@ -38,6 +38,9 @@ import {
|
||||
User as UserIcon,
|
||||
RotateCcw,
|
||||
} from 'lucide-react';
|
||||
import { CreateUserModal } from '../components/users/CreateUserModal';
|
||||
import { UserSecurityModal } from '../components/users/UserSecurityModal';
|
||||
import { useRbac } from '../auth/rbac';
|
||||
import {
|
||||
getUsers,
|
||||
getUser,
|
||||
@@ -97,6 +100,23 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'users' | 'invites' | 'notifications' | 'audit'>('users');
|
||||
|
||||
const { canView, canAdd, canExport } = useRbac();
|
||||
const canViewUsers = canView('settings_users') || canView('tenant_self_service');
|
||||
const canViewInvites = canView('settings_invites');
|
||||
const canViewNotifications = canView('settings_notifications');
|
||||
const canViewAudit = canView('settings_auditlog');
|
||||
|
||||
const canAddUser = canAdd('settings_users') || canAdd('tenant_self_service');
|
||||
const canAddInvite = canAdd('settings_invites');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeTab === 'users' && !canViewUsers) {
|
||||
if (canViewInvites) setActiveTab('invites');
|
||||
else if (canViewNotifications) setActiveTab('notifications');
|
||||
else if (canViewAudit) setActiveTab('audit');
|
||||
}
|
||||
}, [activeTab, canViewUsers, canViewInvites, canViewNotifications, canViewAudit]);
|
||||
|
||||
// ==========================================
|
||||
// 1. User Directory State
|
||||
// ==========================================
|
||||
@@ -105,6 +125,7 @@ export const SettingsPage: React.FC = () => {
|
||||
const [userRoleFilter, setUserRoleFilter] = useState<string>('all');
|
||||
const [selectedUserForDetail, setSelectedUserForDetail] = useState<User | null>(null);
|
||||
const [isUserDetailModalOpen, setIsUserDetailModalOpen] = useState(false);
|
||||
const [isCreateUserModalOpen, setIsCreateUserModalOpen] = useState(false);
|
||||
|
||||
const userFilter: UserFilterRequest = {
|
||||
email: userEmailSearch.trim() || undefined,
|
||||
@@ -572,77 +593,85 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex gap-2 border-b border-slate-800/80 pb-3 overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setActiveTab('users')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'users'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
<span>User Directory ({usersList.length})</span>
|
||||
<HelpTooltip
|
||||
title="User Directory"
|
||||
content="Authorized system users, role memberships, and account lifecycle states."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
{canViewUsers && (
|
||||
<button
|
||||
onClick={() => setActiveTab('users')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'users'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
<span>User Directory ({usersList.length})</span>
|
||||
<HelpTooltip
|
||||
title="User Directory"
|
||||
content="Authorized system users, role memberships, and account lifecycle states."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('invites')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'invites'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
<span>Communications & Invites ({invitesList.length})</span>
|
||||
<HelpTooltip
|
||||
title="Communications Dispatch"
|
||||
content="Statutory onboarding invitations and applicant notification ledger."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
{canViewInvites && (
|
||||
<button
|
||||
onClick={() => setActiveTab('invites')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'invites'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
<span>Communications & Invites ({invitesList.length})</span>
|
||||
<HelpTooltip
|
||||
title="Communications Dispatch"
|
||||
content="Statutory onboarding invitations and applicant notification ledger."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('notifications')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'notifications'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
<span>Gateways & Notifications ({fetchedNotifications.length})</span>
|
||||
<HelpTooltip
|
||||
title="Notification Gateways"
|
||||
content="Real-time status of email dispatch SMTP engines, SMS gateways, and broadcast logs."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
{canViewNotifications && (
|
||||
<button
|
||||
onClick={() => setActiveTab('notifications')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'notifications'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
<span>Gateways & Notifications ({fetchedNotifications.length})</span>
|
||||
<HelpTooltip
|
||||
title="Notification Gateways"
|
||||
content="Real-time status of email dispatch SMTP engines, SMS gateways, and broadcast logs."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('audit')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'audit'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
<span>Audit & Telemetry Ledger ({auditLogs.length})</span>
|
||||
<HelpTooltip
|
||||
title="Audit Trail"
|
||||
content="Comprehensive immutable audit log of administrative actions, statutory approvals, grant disbursements, and security events."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
{canViewAudit && (
|
||||
<button
|
||||
onClick={() => setActiveTab('audit')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap cursor-pointer ${
|
||||
activeTab === 'audit'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
<span>Audit & Telemetry Ledger ({auditLogs.length})</span>
|
||||
<HelpTooltip
|
||||
title="Audit Trail"
|
||||
content="Comprehensive immutable audit log of administrative actions, statutory approvals, grant disbursements, and security events."
|
||||
size="sm"
|
||||
placement="top"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ============================================================== */}
|
||||
@@ -725,12 +754,25 @@ export const SettingsPage: React.FC = () => {
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
|
||||
{canAddUser && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateUserModalOpen(true)}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-500 hover:to-indigo-500 shadow-md shadow-blue-600/25 transition cursor-pointer"
|
||||
title="Provision a new tenant user with Hashids Hex ID and role assignment"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Provision User</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ExportButton
|
||||
title="User Directory & Access Register"
|
||||
subtitle="Canonical directory of authorized users and account states"
|
||||
filename={`pfm-users-directory-${new Date().toISOString().split('T')[0]}`}
|
||||
columns={userExportColumns}
|
||||
data={sortedUsers}
|
||||
claim="settings_users"
|
||||
/>
|
||||
|
||||
<button
|
||||
@@ -830,10 +872,11 @@ export const SettingsPage: React.FC = () => {
|
||||
setSelectedUserForDetail(u);
|
||||
setIsUserDetailModalOpen(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-lg transition cursor-pointer"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-blue-300 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/20 rounded-lg transition cursor-pointer"
|
||||
title="Manage User Roles, Claims, Status, and Security Sessions"
|
||||
>
|
||||
<Eye className="w-3 h-3 text-blue-400" />
|
||||
Inspect
|
||||
<Shield className="w-3 h-3 text-blue-400" />
|
||||
Manage Access
|
||||
</button>
|
||||
|
||||
{/* Status switch actions */}
|
||||
@@ -948,15 +991,18 @@ export const SettingsPage: React.FC = () => {
|
||||
filename={`pfm-sent-invitations-${new Date().toISOString().split('T')[0]}`}
|
||||
columns={inviteExportColumns}
|
||||
data={sortedInvites}
|
||||
claim="settings_invites"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => setIsSendInviteModalOpen(true)}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-500 hover:to-indigo-500 shadow-md shadow-blue-600/25 transition cursor-pointer"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>Dispatch Invitation</span>
|
||||
</button>
|
||||
{canAddInvite && (
|
||||
<button
|
||||
onClick={() => setIsSendInviteModalOpen(true)}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-500 hover:to-indigo-500 shadow-md shadow-blue-600/25 transition cursor-pointer"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
<span>Dispatch Invitation</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1227,6 +1273,7 @@ export const SettingsPage: React.FC = () => {
|
||||
filename={`pfm-notifications-org-${effectiveNotificationOrgId}-${new Date().toISOString().split('T')[0]}`}
|
||||
columns={notificationExportColumns}
|
||||
data={filteredNotifications}
|
||||
claim="settings_notifications"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1493,6 +1540,7 @@ export const SettingsPage: React.FC = () => {
|
||||
filename={`pfm-audit-trail-${new Date().toISOString().split('T')[0]}`}
|
||||
columns={auditExportColumns}
|
||||
data={filteredAuditLogs}
|
||||
claim="settings_auditlog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2092,73 +2140,24 @@ export const SettingsPage: React.FC = () => {
|
||||
</Modal>
|
||||
|
||||
{/* ============================================================== */}
|
||||
{/* Modal: User Inspection */}
|
||||
{/* Modal: User Administration & Role/Claim/Session Control */}
|
||||
{/* ============================================================== */}
|
||||
<Modal
|
||||
<UserSecurityModal
|
||||
isOpen={isUserDetailModalOpen}
|
||||
onClose={() => setIsUserDetailModalOpen(false)}
|
||||
title="User Access & Security Details"
|
||||
>
|
||||
{selectedUserForDetail && (
|
||||
<div className="space-y-4 text-xs">
|
||||
{loadingUserFreshDetail ? (
|
||||
<div className="p-6 text-center text-slate-400">
|
||||
<RefreshCw className="w-5 h-5 animate-spin mx-auto mb-2 text-blue-400" />
|
||||
Fetching user profile details...
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-slate-900/60 rounded-xl border border-slate-800 space-y-2">
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User Email:</span>
|
||||
<span className="text-white font-semibold text-xs">
|
||||
{userFreshDetail?.email || selectedUserForDetail.email || getUserEmail(selectedUserForDetail.id) || 'Not specified'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User Identifier:</span>
|
||||
<span className="font-mono text-slate-300 text-xs break-all">
|
||||
{userFreshDetail?.id || selectedUserForDetail.id}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Id Number:</span>
|
||||
<span className="font-mono text-slate-200 text-xs">
|
||||
{userFreshDetail?.idNumber || selectedUserForDetail.idNumber || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase mb-1">System Role:</span>
|
||||
<UserRoleBadge role={userFreshDetail?.role ?? selectedUserForDetail.role} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Status:</span>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border ${getActivityStatusBadge(userFreshDetail?.status ?? selectedUserForDetail.status).className}`}>
|
||||
{getActivityStatusBadge(userFreshDetail?.status ?? selectedUserForDetail.status).label}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Last Login:</span>
|
||||
<span className="font-mono text-slate-300">
|
||||
{(userFreshDetail?.lastLoginAt || selectedUserForDetail.lastLoginAt) ? formatDate((userFreshDetail?.lastLoginAt || selectedUserForDetail.lastLoginAt)!) : 'Never'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
onClose={() => {
|
||||
setIsUserDetailModalOpen(false);
|
||||
setSelectedUserForDetail(null);
|
||||
}}
|
||||
user={selectedUserForDetail}
|
||||
/>
|
||||
|
||||
<div className="pt-2 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsUserDetailModalOpen(false)}
|
||||
className="px-4 py-2 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 rounded-xl transition"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
{/* ============================================================== */}
|
||||
{/* Modal: Provision Tenant User */}
|
||||
{/* ============================================================== */}
|
||||
<CreateUserModal
|
||||
isOpen={isCreateUserModalOpen}
|
||||
onClose={() => setIsCreateUserModalOpen(false)}
|
||||
/>
|
||||
|
||||
{/* ============================================================== */}
|
||||
{/* Modal: Notification Payload & Data Inspection */}
|
||||
|
||||
Reference in New Issue
Block a user