Overhauled lockout process

This commit is contained in:
2026-10-24 17:36:30 +02:00
parent a731c979bb
commit 25e251aa79
12 changed files with 574 additions and 527 deletions
@@ -115,141 +115,94 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
navigate('/');
};
const isTenantIssue = !!errorMessage?.toLowerCase().includes('tenant');
const modalContent = (
<div
role="dialog"
aria-modal="true"
aria-labelledby="activation-modal-title"
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 sm:p-6 bg-[#060a12]/90 backdrop-blur-md animate-in fade-in duration-200 select-none overflow-y-auto"
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-[#060a12]/85 backdrop-blur-md animate-in fade-in duration-200 select-none overflow-y-auto"
>
{/* Non-dismissable Card Container: No X button, no escape dismiss, no backdrop click */}
<div
className="relative w-full max-w-xl bg-[#0d1322] border border-amber-500/30 rounded-2xl shadow-2xl shadow-black/80 overflow-hidden flex flex-col text-slate-100 my-auto"
className="relative w-full max-w-lg bg-[#0d1322] border border-slate-800 rounded-2xl shadow-2xl shadow-black/80 overflow-hidden flex flex-col text-slate-100 my-auto"
onClick={(e) => e.stopPropagation()}
>
{/* Amber accent top bar */}
<div className="h-1.5 w-full bg-gradient-to-r from-amber-500 via-amber-400 to-yellow-500" />
{/* Subtle accent top border */}
<div className={`h-1 w-full ${isTenantIssue ? 'bg-amber-500' : 'bg-blue-600'}`} />
{/* Modal Header */}
<div className="p-6 pb-4 border-b border-slate-800/80 bg-[#0f172a]/60">
{/* Header */}
<div className="p-5 pb-4 border-b border-slate-800/80 bg-[#0f172a]/60">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3.5">
<div className="relative p-2.5 rounded-xl bg-amber-500/10 border border-amber-500/20 text-amber-400">
<ShieldAlert className="w-6 h-6" />
<span className="absolute -top-1 -right-1 flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-amber-500"></span>
</span>
<div className="flex items-center gap-3">
<div className={`p-2.5 rounded-xl border ${isTenantIssue ? 'bg-amber-500/10 border-amber-500/20 text-amber-400' : 'bg-blue-500/10 border-blue-500/20 text-blue-400'}`}>
<ShieldAlert className="w-5 h-5" />
</div>
<div>
<span className="inline-flex items-center gap-1.5 text-[11px] font-semibold tracking-wider uppercase text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2 py-0.5 rounded-full mb-1">
Access Restricted • Lockout Active
</span>
<h2 id="activation-modal-title" className="text-lg sm:text-xl font-bold text-white tracking-tight">
{errorMessage?.toLowerCase().includes('tenant')
? 'Tenant Lockout • Account Inactive'
: 'Account Pending Administrator Activation'}
<h2 id="activation-modal-title" className="text-base font-bold text-white tracking-tight">
{isTenantIssue ? 'Tenant Access Restricted' : 'Account Pending Activation'}
</h2>
<p className="text-xs text-slate-400 mt-0.5">
{isTenantIssue
? 'The assigned tenant partition is currently inactive or restricted.'
: 'Your account is authenticated and awaiting administrator enablement.'}
</p>
</div>
</div>
<BrandLogo size={36} showGlow={false} animateOnHover={false} />
<BrandLogo size={32} showGlow={false} animateOnHover={false} />
</div>
</div>
{/* Modal Body */}
<div className="p-6 space-y-5 overflow-y-auto max-h-[70vh]">
{/* Status notification block */}
<div className="bg-slate-900/80 border border-slate-800 rounded-xl p-4 space-y-2.5">
<div className="flex items-center gap-2 text-emerald-400 text-xs font-semibold">
<CheckCircle2 className="w-4 h-4 shrink-0" />
<span>Identity Verified & Signed In Successfully</span>
{/* Body */}
<div className="p-5 space-y-4">
{/* User Profile Summary */}
<div className="rounded-xl border border-slate-800/80 bg-slate-900/60 p-3.5 space-y-2 text-xs">
<div className="grid grid-cols-2 gap-2.5">
<div>
<span className="text-[10px] uppercase font-semibold text-slate-500 block">User</span>
<span className="font-semibold text-white truncate block">{displayName}</span>
</div>
<div>
<span className="text-[10px] uppercase font-semibold text-slate-500 block">Email</span>
<span className="text-slate-300 truncate block">{displayEmail || '—'}</span>
</div>
</div>
<p className="text-xs text-slate-300 leading-relaxed">
You are a registered user and have successfully authenticated via Enterprise Single Sign-On.
However, your account has not yet been enabled in the system directory by a system administrator.
</p>
<p className="text-xs text-slate-400 leading-relaxed border-t border-slate-800/80 pt-2 font-medium">
If your administrator informs you that your account has been activated, please click the{' '}
<strong className="text-white">Sync Account</strong> button below to refresh your status and unlock the system.
</p>
</div>
{/* User Profile Card */}
<div className="rounded-xl border border-slate-800 bg-[#090e1a]/60 p-4 space-y-3">
<div className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider flex items-center justify-between">
<span>Authenticated User Profile</span>
<span className="text-amber-400/90 text-[10px] font-mono bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">
Lockout Active
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
<div className="flex items-center gap-2.5 bg-slate-900/60 p-2.5 rounded-lg border border-slate-800/60">
<UserCheck className="w-4 h-4 text-blue-400 shrink-0" />
<div className="min-w-0">
<span className="text-[10px] text-slate-500 block">Name</span>
<span className="font-semibold text-white truncate block">{displayName}</span>
</div>
<div className="grid grid-cols-2 gap-2.5 border-t border-slate-800/60 pt-2">
<div>
<span className="text-[10px] uppercase font-semibold text-slate-500 block">Tenant</span>
<span className="text-slate-300 truncate block">{tenant}</span>
</div>
<div className="flex items-center gap-2.5 bg-slate-900/60 p-2.5 rounded-lg border border-slate-800/60">
<Mail className="w-4 h-4 text-blue-400 shrink-0" />
<div className="min-w-0">
<span className="text-[10px] text-slate-500 block">Email</span>
<span className="font-semibold text-slate-200 truncate block">{displayEmail || 'Not specified'}</span>
</div>
</div>
<div className="flex items-center gap-2.5 bg-slate-900/60 p-2.5 rounded-lg border border-slate-800/60">
<Building2 className="w-4 h-4 text-indigo-400 shrink-0" />
<div className="min-w-0">
<span className="text-[10px] text-slate-500 block">Tenant / Role</span>
<span className="font-semibold text-slate-200 truncate block">
{tenant} ({role})
</span>
</div>
</div>
<div className="flex items-center gap-2.5 bg-slate-900/60 p-2.5 rounded-lg border border-slate-800/60">
<Fingerprint className="w-4 h-4 text-slate-400 shrink-0" />
<div className="min-w-0">
<span className="text-[10px] text-slate-500 block">Subject ID</span>
<span className="font-mono text-[11px] text-slate-400 truncate block">{sub || 'SSO-Principal'}</span>
</div>
<div>
<span className="text-[10px] uppercase font-semibold text-slate-500 block">Role</span>
<span className="text-blue-400 font-medium truncate block">{role}</span>
</div>
</div>
</div>
{/* Sync Attempt Feedback / Error Notice */}
{/* Feedback notice if sync failed */}
{errorMessage && (
<div className="rounded-xl border border-rose-500/30 bg-rose-500/10 p-3.5 flex items-start gap-3 animate-in fade-in duration-150">
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-xs font-semibold text-rose-300">
{errorMessage?.toLowerCase().includes('tenant')
? 'Tenant Lockout In Effect'
: 'Account Not Activated Yet'}
</p>
<p className="text-xs text-rose-200/90 leading-relaxed">{errorMessage}</p>
{syncAttemptCount > 0 && (
<p className="text-[11px] text-rose-400/80 pt-1">
Attempt #{syncAttemptCount} checked at {new Date().toLocaleTimeString()}
</p>
)}
<div className="rounded-xl border border-amber-500/25 bg-amber-500/10 p-3 flex items-start gap-2.5 text-xs text-amber-200">
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<div className="min-w-0">
<span className="font-semibold text-amber-300 block">Notice:</span>
<p className="text-[11px] text-amber-200/90 leading-relaxed mt-0.5">{errorMessage}</p>
</div>
</div>
)}
<p className="text-[11px] text-slate-400 text-center">
Once your administrator enables your account, click <strong className="text-white">Sync Account</strong> to enter the console.
</p>
</div>
{/* Modal Actions */}
<div className="p-5 border-t border-slate-800/80 bg-[#0a0f1d] flex flex-col sm:flex-row items-center justify-between gap-3">
{/* Footer Actions */}
<div className="p-4 border-t border-slate-800 bg-[#0a0f1d] flex flex-col sm:flex-row items-center justify-between gap-2.5">
<div className="flex items-center gap-2 w-full sm:w-auto">
<button
onClick={handleNavigateLanding}
type="button"
className="flex-1 sm:flex-none inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs font-medium text-slate-400 hover:text-slate-200 bg-slate-900 hover:bg-slate-800 border border-slate-800 rounded-xl transition cursor-pointer"
title="View public landing page. Entering the console again will display this modal until enabled."
className="flex-1 sm:flex-none inline-flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium text-slate-400 hover:text-white bg-slate-900 hover:bg-slate-800 border border-slate-800 rounded-xl transition cursor-pointer"
>
<ArrowLeft className="w-3.5 h-3.5" />
<span>Landing Page</span>
@@ -258,8 +211,7 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
<button
onClick={handleSignOut}
type="button"
className="flex-1 sm:flex-none inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs font-medium text-slate-400 hover:text-rose-400 bg-slate-900 hover:bg-rose-500/10 border border-slate-800 hover:border-rose-500/30 rounded-xl transition cursor-pointer"
title="Sign out of current SSO session"
className="flex-1 sm:flex-none inline-flex items-center justify-center gap-1.5 px-3 py-2 text-xs font-medium text-slate-400 hover:text-rose-400 bg-slate-900 hover:bg-rose-500/10 border border-slate-800 hover:border-rose-500/20 rounded-xl transition cursor-pointer"
>
<LogOut className="w-3.5 h-3.5" />
<span>Sign Out</span>
@@ -271,10 +223,10 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
disabled={isSyncing}
type="button"
id="activation-sync-btn"
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-5 py-2.5 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 active:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none rounded-xl shadow-lg shadow-blue-600/30 transition cursor-pointer"
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-4 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 active:bg-blue-700 disabled:opacity-50 rounded-xl shadow-md transition cursor-pointer"
>
<RefreshCw className={`w-4 h-4 ${isSyncing ? 'animate-spin' : ''}`} />
<span>{isSyncing ? 'Verifying Activation...' : 'Sync Account'}</span>
<RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} />
<span>{isSyncing ? 'Checking...' : 'Sync Account'}</span>
</button>
</div>
</div>
@@ -61,114 +61,57 @@ export const PartnerAccessDeniedModal: React.FC<PartnerAccessDeniedModalProps> =
aria-modal="true"
aria-labelledby="partner-denied-title"
aria-describedby="partner-denied-description"
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/85 backdrop-blur-md animate-in fade-in duration-200 overflow-y-auto"
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/80 backdrop-blur-md animate-in fade-in duration-150 overflow-y-auto"
>
<div className="relative w-full max-w-lg overflow-hidden rounded-2xl bg-[#0e1424] border border-rose-900/60 shadow-2xl shadow-rose-950/50 my-auto">
{/* Glow Header Accent */}
<div className="h-2 w-full bg-linear-to-r from-rose-600 via-red-500 to-amber-500" />
<div className="relative w-full max-w-md overflow-hidden rounded-2xl bg-[#0d1322] border border-slate-800 shadow-2xl my-auto text-slate-100">
<div className="h-1 w-full bg-rose-600" />
<div className="p-6 sm:p-8 space-y-6">
{/* Header */}
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-rose-500/15 border border-rose-500/30 text-rose-400">
<Ban className="h-6 w-6 text-rose-400" />
<div className="p-5 space-y-4">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-rose-500/10 border border-rose-500/20 text-rose-400">
<Ban className="h-5 w-5" />
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1 text-[11px] font-semibold text-rose-400 bg-rose-950/60 px-2 py-0.5 rounded-md border border-rose-800/60">
<Lock className="w-3 h-3" />
Restricted Role
</span>
<span className="text-xs text-slate-500">Security Policy</span>
</div>
<h2 id="partner-denied-title" className="text-lg font-bold text-white tracking-tight">
Access Denied: Partner Role
<div>
<h2 id="partner-denied-title" className="text-base font-bold text-white tracking-tight">
Partner Portal Required
</h2>
<p id="partner-denied-description" className="text-xs text-slate-400 leading-relaxed">
Users assigned the <strong className="text-rose-300">Partner</strong> role by security are not permitted to access the PFM Backoffice Console.
<p id="partner-denied-description" className="text-xs text-slate-400 mt-0.5">
Accounts with the Partner role must access the Partner Portal.
</p>
</div>
</div>
{/* User & Identity Summary Card */}
<div className="rounded-xl bg-slate-900/80 border border-slate-800 p-4 space-y-3 text-xs">
<div className="flex items-center justify-between pb-2 border-b border-slate-800/80">
<span className="text-slate-400 font-medium">Account Name</span>
<span className="text-white font-semibold">{displayName}</span>
<div className="rounded-xl bg-slate-900/60 border border-slate-800 p-3.5 space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-slate-500 text-[11px] uppercase font-semibold">User</span>
<span className="text-white font-medium">{displayName}</span>
</div>
{displayEmail && (
<div className="flex items-center justify-between pb-2 border-b border-slate-800/80">
<span className="text-slate-400 font-medium flex items-center gap-1.5">
<Mail className="w-3.5 h-3.5 text-slate-500" />
Email
</span>
<span className="text-slate-200 font-mono">{displayEmail}</span>
<div className="flex items-center justify-between border-t border-slate-800/60 pt-2">
<span className="text-slate-500 text-[11px] uppercase font-semibold">Email</span>
<span className="text-slate-300 font-mono text-[11px]">{displayEmail}</span>
</div>
)}
<div className="flex items-center justify-between pb-2 border-b border-slate-800/80">
<span className="text-slate-400 font-medium flex items-center gap-1.5">
<Building2 className="w-3.5 h-3.5 text-slate-500" />
Assigned Role
</span>
<span className="text-rose-400 font-semibold bg-rose-950/60 px-2 py-0.5 rounded border border-rose-800/50">
{role}
</span>
<div className="flex items-center justify-between border-t border-slate-800/60 pt-2">
<span className="text-slate-500 text-[11px] uppercase font-semibold">Role</span>
<span className="text-rose-400 font-medium">{role}</span>
</div>
{tenant && (
<div className="flex items-center justify-between pb-2 border-b border-slate-800/80">
<span className="text-slate-400 font-medium flex items-center gap-1.5">
<Building2 className="w-3.5 h-3.5 text-slate-500" />
Tenant Context
</span>
<span className="text-slate-200 font-medium truncate max-w-[220px]">{tenant}</span>
</div>
)}
{sub && (
<div className="flex items-center justify-between text-[11px] text-slate-500">
<span className="flex items-center gap-1">
<Fingerprint className="w-3 h-3" />
Subject Key
</span>
<span className="font-mono text-slate-400 truncate max-w-[200px]">{sub}</span>
</div>
)}
</div>
{/* Security Policy Information */}
<div className="p-4 rounded-xl bg-rose-950/20 border border-rose-900/40 text-xs text-rose-200 leading-relaxed space-y-1.5">
<p className="font-semibold text-rose-300 flex items-center gap-1.5">
<ShieldAlert className="w-4 h-4 text-rose-400" />
Administrative Boundary Notice
</p>
<p className="text-[11px] text-rose-300/90">
The PFM Console is reserved for statutory administrators, sponsors, programme managers, and governance officers. Partner credentials cannot access this environment. If you require backoffice administrative access, please request an updated role assignment from your system administrator.
</p>
</div>
<p className="text-[11px] text-slate-400 text-center leading-relaxed">
The Console is reserved for statutory administrators and programme managers.
</p>
{/* Actions */}
<div className="pt-2">
<div className="pt-1">
<button
onClick={handleLogoutClick}
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 hover:text-white text-xs font-semibold border border-slate-700 transition cursor-pointer shadow-sm"
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold shadow-md transition cursor-pointer"
>
<LogOut className="w-4 h-4 text-rose-400" />
<span>Sign Out & Return to Home</span>
<LogOut className="w-4 h-4" />
<span>Sign Out & Return Home</span>
</button>
</div>
</div>
{/* Footer Brand watermark */}
<div className="px-6 py-3 bg-[#0a0f1d] border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-500">
<div className="flex items-center gap-2">
<BrandLogo size={16} showGlow={false} animateOnHover={false} />
<span>Post Fund Management (PFM)</span>
</div>
<span>National Security Gateway</span>
</div>
</div>
</div>
);
@@ -87,7 +87,7 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
const modalContent = (
<div
id="session-expired-modal-backdrop"
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/85 backdrop-blur-md p-4 animate-in fade-in duration-200 overflow-y-auto"
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-md p-4 animate-in fade-in duration-150 overflow-y-auto"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
role="dialog"
@@ -96,27 +96,20 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
>
<div
id="session-expired-modal-content"
className="relative w-full max-w-md bg-[#0c121e] border border-amber-500/40 rounded-2xl shadow-2xl shadow-black/90 p-6 overflow-hidden text-slate-200 space-y-5 my-auto"
className="relative w-full max-w-md bg-[#0d1322] border border-slate-800 rounded-2xl shadow-2xl p-5 overflow-hidden text-slate-200 space-y-4 my-auto"
>
{/* Subtle decorative top accent bar */}
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-amber-500 via-orange-500 to-rose-500" />
<div className="absolute top-0 left-0 right-0 h-1 bg-amber-500" />
{/* Top bar with close button */}
<div className="flex items-start justify-between pt-1">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-amber-500/10 border border-amber-500/30 flex items-center justify-center text-amber-400 shrink-0 shadow-lg shadow-amber-500/10">
<ShieldAlert className="w-6 h-6" />
<div className="w-10 h-10 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-400 shrink-0">
<ShieldAlert className="w-5 h-5" />
</div>
<div>
<div className="flex items-center gap-2">
<h3 id="session-expired-title" className="text-base font-bold text-white tracking-tight">
Session Expired
</h3>
<span className="text-[10px] font-mono font-semibold px-2 py-0.5 rounded bg-rose-950/60 text-rose-300 border border-rose-800/60">
HTTP 401
</span>
</div>
<p className="text-xs text-slate-400 mt-0.5">Authorization token invalidated</p>
<h3 id="session-expired-title" className="text-base font-bold text-white tracking-tight">
Session Expired
</h3>
<p className="text-xs text-slate-400">Authorization credentials expired</p>
</div>
</div>
@@ -125,60 +118,46 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
type="button"
onClick={handleAction}
aria-label="Dismiss and return to landing page"
className="p-1.5 text-slate-400 hover:text-white rounded-lg hover:bg-slate-800/80 transition-colors"
className="p-1.5 text-slate-400 hover:text-white rounded-lg hover:bg-slate-800 transition-colors cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Informative Explanation (non-technical and clear) */}
<div className="space-y-2.5 text-xs text-slate-300 leading-relaxed bg-slate-900/60 p-4 rounded-xl border border-slate-800/80">
<div className="text-xs text-slate-300 leading-relaxed bg-slate-900/60 p-3.5 rounded-xl border border-slate-800/80 space-y-1.5">
<p>
Your active working session has ended or your security credentials have expired. To protect your data and prevent unauthorized changes, your session has been closed.
Your active session has ended to protect account security. Please sign in again to continue working.
</p>
{reason && (
<div className="flex items-start gap-2 pt-1 text-[11px] text-amber-300/90 font-mono">
<div className="flex items-start gap-1.5 pt-1 text-[11px] text-amber-300/90 font-mono">
<AlertTriangle className="w-3.5 h-3.5 shrink-0 mt-0.5 text-amber-400" />
<span className="break-words">{reason}</span>
</div>
)}
</div>
{/* Progress Bar Countdown */}
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-slate-400 flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-amber-400" />
Returning to landing page in{' '}
<strong className="text-white font-mono">{secondsRemaining}s</strong>
<div className="flex items-center justify-between text-xs text-slate-400">
<span className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-amber-400" />
Redirecting in <strong className="text-white font-mono">{secondsRemaining}s</strong>
</span>
{isPaused && (
<span className="text-[10px] text-amber-400/80 px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20">
Paused
</span>
{isPaused && (
<span className="text-[10px] text-amber-400/80 font-medium px-1.5 py-0.2 rounded bg-amber-950/40 border border-amber-900/60">
Paused on hover
</span>
)}
</div>
{/* Progress bar line */}
<div className="w-full h-2 bg-slate-800/90 rounded-full overflow-hidden border border-slate-700/60 p-0.5">
<div
className="h-full rounded-full bg-gradient-to-r from-amber-500 via-orange-500 to-rose-500 transition-all duration-75 ease-linear"
style={{ width: `${progressPercent}%` }}
/>
</div>
)}
</div>
{/* Actions */}
<div className="pt-2 flex flex-col sm:flex-row items-center gap-2.5">
<div className="pt-1">
<button
id="session-expired-return-btn"
type="button"
onClick={handleAction}
className="w-full py-2.5 px-4 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition-all flex items-center justify-center gap-2"
className="w-full py-2.5 px-4 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl shadow-md transition-all flex items-center justify-center gap-2 cursor-pointer"
>
<LogOut className="w-4 h-4" />
<span>Return to Landing Page Now</span>
<ArrowRight className="w-3.5 h-3.5 ml-0.5" />
<span>Return to Landing Page</span>
<ArrowRight className="w-3.5 h-3.5" />
</button>
</div>
</div>
@@ -36,7 +36,7 @@ import {
getAllProgrammes,
getProjects,
} from '../../services/api';
import { toolsService } from '../../services/toolsService';
import { toolsService } from '../../utils/hashids';
import { Modal } from '../ui/Modal';
import { useToast } from '../ui/Toast';
import { ProblemDetailsAlert } from '../ui/ProblemDetailsAlert';
@@ -290,18 +290,18 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
<Modal
isOpen={isOpen}
onClose={onClose}
title={`User Administration: ${effectiveUser.email || effectiveUser.id}`}
subtitle="Direct tenant-level user, role, claims, and session administration without database intervention."
title={`User Security: ${effectiveUser.email || effectiveUser.id}`}
subtitle="Manage account status, role assignment, permissions, and active sessions."
>
<div className="space-y-4 text-xs">
{/* Navigation Tabs */}
<div className="flex gap-2 border-b border-slate-800 pb-2">
<div className="flex gap-1.5 border-b border-slate-800 pb-2">
<button
type="button"
onClick={() => setActiveTab('overview')}
className={`px-3 py-1.5 rounded-lg font-semibold transition cursor-pointer ${
className={`px-3 py-1.5 rounded-lg font-medium transition cursor-pointer ${
activeTab === 'overview'
? 'bg-blue-600 text-white'
? 'bg-blue-600 text-white font-semibold'
: 'text-slate-400 hover:text-white hover:bg-slate-800'
}`}
>
@@ -310,21 +310,21 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
<button
type="button"
onClick={() => setActiveTab('roles')}
className={`px-3 py-1.5 rounded-lg font-semibold transition cursor-pointer flex items-center gap-1.5 ${
className={`px-3 py-1.5 rounded-lg font-medium transition cursor-pointer flex items-center gap-1.5 ${
activeTab === 'roles'
? 'bg-blue-600 text-white'
? 'bg-blue-600 text-white font-semibold'
: 'text-slate-400 hover:text-white hover:bg-slate-800'
}`}
>
<Shield className="w-3.5 h-3.5" />
<span>Roles ({(Array.isArray(userRoles) ? userRoles : []).length})</span>
<span>Role ({(Array.isArray(userRoles) ? userRoles : []).length})</span>
</button>
<button
type="button"
onClick={() => setActiveTab('claims')}
className={`px-3 py-1.5 rounded-lg font-semibold transition cursor-pointer flex items-center gap-1.5 ${
className={`px-3 py-1.5 rounded-lg font-medium transition cursor-pointer flex items-center gap-1.5 ${
activeTab === 'claims'
? 'bg-blue-600 text-white'
? 'bg-blue-600 text-white font-semibold'
: 'text-slate-400 hover:text-white hover:bg-slate-800'
}`}
>
@@ -334,9 +334,9 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
<button
type="button"
onClick={() => setActiveTab('sessions')}
className={`px-3 py-1.5 rounded-lg font-semibold transition cursor-pointer flex items-center gap-1.5 ${
className={`px-3 py-1.5 rounded-lg font-medium transition cursor-pointer flex items-center gap-1.5 ${
activeTab === 'sessions'
? 'bg-blue-600 text-white'
? 'bg-blue-600 text-white font-semibold'
: 'text-slate-400 hover:text-white hover:bg-slate-800'
}`}
>
@@ -602,55 +602,43 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
</div>
)}
{/* TAB 4: Sessions & Direct Account Interventions */}
{/* TAB 4: Sessions & Security Interventions */}
{activeTab === 'sessions' && (
<div className="space-y-3">
<div className="p-4 bg-slate-900/60 rounded-xl border border-slate-800 space-y-4">
{/* Force Logout Action */}
<div className="flex items-center justify-between border-b border-slate-800/80 pb-3">
<div className="flex items-center justify-between border-b border-slate-800/80 pb-3 gap-3">
<div>
<h4 className="text-white font-semibold flex items-center gap-1.5">
<LogOut className="w-4 h-4 text-amber-400" />
<span>Revoke Active User Session</span>
<HelpTooltip
title="User Session Invalidation"
content="Enables a tenant self-service user to immediately logout a user if they are currently logged in, terminating their active token and session."
size="sm"
placement="top"
/>
</h4>
<p className="text-[11px] text-slate-400 mt-0.5">
Terminates all active JWT tokens and sessions immediately without direct database intervention.
Immediately invalidates active JWT tokens and logs the user out.
</p>
</div>
<button
type="button"
onClick={() => logoutMutation.mutate()}
disabled={logoutMutation.isPending || !canManageSessions}
className="px-3 py-1.5 bg-amber-500/10 hover:bg-amber-500/20 text-amber-400 border border-amber-500/20 rounded-xl font-semibold transition cursor-pointer disabled:opacity-50"
className="px-3 py-1.5 bg-amber-500/10 hover:bg-amber-500/20 text-amber-400 border border-amber-500/20 rounded-xl font-semibold transition cursor-pointer disabled:opacity-50 shrink-0"
>
{logoutMutation.isPending ? 'Revoking...' : 'Force Logout'}
</button>
</div>
{/* Lock / Unlock Account */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between gap-3">
<div>
<h4 className="text-white font-semibold flex items-center gap-1.5">
<Lock className="w-4 h-4 text-rose-400" />
<span>Account Lockout Control</span>
<HelpTooltip
title="Account Lockout Control"
content="Enables a tenant self-service user to change a user's lockout status. The system defaults to a lockout period of 100 years when locking a user account."
size="sm"
placement="top"
/>
<span>Account Lockout</span>
</h4>
<p className="text-[11px] text-slate-400 mt-0.5">
Temporarily lock out or re-enable the user from logging into this tenant. The system defaults to a lockout period of 100 years.
Control tenant login authorization for this account.
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={(e) => {