180 lines
7.5 KiB
TypeScript
180 lines
7.5 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
ShieldAlert,
|
|
LogOut,
|
|
Ban,
|
|
Mail,
|
|
Building2,
|
|
Fingerprint,
|
|
Lock,
|
|
} from 'lucide-react';
|
|
import { BrandLogo } from '../ui/BrandLogo';
|
|
import { AuthUserProfile, UserClaims } from '../../auth/AuthProvider';
|
|
import { cancelSessionExpiration, dismissSubordinateModals } from '../../services/sessionExpiration';
|
|
|
|
interface NonPartnerAccessDeniedModalProps {
|
|
user: AuthUserProfile | null;
|
|
claims: UserClaims;
|
|
sub: string;
|
|
roles: string[];
|
|
onLogout: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Blocks any authenticated user who does NOT hold the 'Partner' role.
|
|
* RBAC on PartnerUI strictly permits only Partner roles; no other roles are allowed.
|
|
*/
|
|
export const NonPartnerAccessDeniedModal: React.FC<NonPartnerAccessDeniedModalProps> = ({
|
|
user,
|
|
claims,
|
|
sub,
|
|
roles,
|
|
onLogout,
|
|
}) => {
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
cancelSessionExpiration();
|
|
dismissSubordinateModals();
|
|
}, []);
|
|
|
|
const displayName =
|
|
user?.name ||
|
|
(claims.name as string) ||
|
|
(claims.preferred_username as string) ||
|
|
'Authenticated User';
|
|
|
|
const displayEmail =
|
|
user?.email ||
|
|
(claims.email as string) ||
|
|
(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] as string) ||
|
|
'';
|
|
|
|
const tenant = user?.tenant || (claims.tenant as string) || (claims.org as string) || 'Statutory Enterprise';
|
|
const assignedRolesDisplay = roles.length > 0 ? roles.join(', ') : user?.role || (claims.role as string) || 'Unassigned';
|
|
|
|
const handleLogoutClick = async () => {
|
|
await onLogout();
|
|
navigate('/', { replace: true });
|
|
};
|
|
|
|
const modalContent = (
|
|
<div
|
|
role="alertdialog"
|
|
aria-modal="true"
|
|
aria-labelledby="non-partner-denied-title"
|
|
aria-describedby="non-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"
|
|
>
|
|
<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">
|
|
<div className="h-2 w-full bg-gradient-to-r from-rose-600 via-red-500 to-amber-500" />
|
|
|
|
<div className="p-6 sm:p-8 space-y-6">
|
|
<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>
|
|
<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" />
|
|
Partner Role Required
|
|
</span>
|
|
<span className="text-xs text-slate-500">Security Policy</span>
|
|
</div>
|
|
<h2 id="non-partner-denied-title" className="text-lg font-bold text-white tracking-tight">
|
|
Access Denied: Partner Accounts Only
|
|
</h2>
|
|
<p id="non-partner-denied-description" className="text-xs text-slate-400 leading-relaxed">
|
|
This application is exclusively reserved for users assigned the <strong className="text-emerald-300">Partner</strong> role. Your account role (<strong className="text-rose-300">{assignedRolesDisplay}</strong>) is not permitted on the Partner Portal.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
{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>
|
|
)}
|
|
|
|
<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(s)
|
|
</span>
|
|
<span className="text-rose-400 font-semibold bg-rose-950/60 px-2 py-0.5 rounded border border-rose-800/50">
|
|
{assignedRolesDisplay}
|
|
</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" />
|
|
Organisation / Tenant
|
|
</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>
|
|
|
|
<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" />
|
|
Portal Separation Notice
|
|
</p>
|
|
<p className="text-[11px] text-rose-300/90">
|
|
Internal staff, executives, programme managers, and administrators must sign in through the PFM Management Console. Only external grant recipients and project partners with the <strong>Partner</strong> role are permitted to use this portal.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-2">
|
|
<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"
|
|
>
|
|
<LogOut className="w-4 h-4 text-rose-400" />
|
|
<span>Sign Out & Return to Home</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<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>PFM Partner Portal</span>
|
|
</div>
|
|
<span>Role Security Gate</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return typeof document !== 'undefined'
|
|
? createPortal(modalContent, document.body)
|
|
: modalContent;
|
|
};
|
|
|