From 7220700bac472014cb82b6405af3b63e9a6ab675 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 24 Oct 2026 11:39:47 +0200 Subject: [PATCH 1/2] Removed setUserLockout from PartnerUI --- .../Endpoints/Users/SetUserLockoutEndpoint.cs | 43 ++++--- PostFundManagement.Api/Program.cs | 4 +- .../Extensions/General.cs | 29 +++-- .../src/auth/AuthProvider.tsx | 105 +----------------- .../src/services/api/index.ts | 8 +- .../src/services/userSyncService.ts | 55 +++------ 6 files changed, 67 insertions(+), 177 deletions(-) diff --git a/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs index c58aac9..b9d7521 100644 --- a/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Extensions; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; @@ -13,22 +13,29 @@ public sealed class SetUserLockoutEndpoint : IEndpoint { builder.MapPut("api/users/{userId:Guid}/session/lockout/{disable:bool}", async (Guid userId, bool disable, HttpContext context, IUserService service, CancellationToken cancellationToken = default) => - await service.SetUserLockoutAsync(userId, context.GetUserId(), disable, cancellationToken) switch - { - { IsSuccess: true } => Results.Ok(), - var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), - var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), - var r => Results.InternalServerError(r.Errors.ToProblems()) - }) - .WithDescription("Lock user session") - .RequireAuthorization(Constants.MainScope) - .WithName(typeof(SetUserLockoutEndpoint).ToEndpointName()) - .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status401Unauthorized) - .Produces(StatusCodes.Status404NotFound) - .Produces(StatusCodes.Status400BadRequest) - .Produces(StatusCodes.Status500InternalServerError) - .WithTags(EndpointTags.Users); + { + var (_, role) = context.User.GetUserRole(); + if (role != null && role.Equals("Partner", StringComparison.OrdinalIgnoreCase)) + return Results.Forbid(); + + return await service.SetUserLockoutAsync(userId, context.GetUserId(), disable, cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }; + }) + .WithDescription("Lock user session") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(SetUserLockoutEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); } } diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index 6bbab29..c44f968 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -29,7 +29,7 @@ builder.Services.AddApiVersioning(options => builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddTransient(); +builder.Services.AddScoped(); builder.Services.AddEndpoints(Assembly.GetExecutingAssembly()); builder.Services.AddApiServices(builder.Configuration); @@ -218,4 +218,4 @@ app.MapGet("/", () => Results.Redirect($"/openapi/{latestVersionGroup}")) if (!app.Environment.IsDevelopment()) app.UseExceptionHandler("/Error", createScopeForErrors: true); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/PostFundManagement.Application/Extensions/General.cs b/PostFundManagement.Application/Extensions/General.cs index ec14c53..91e7fdb 100644 --- a/PostFundManagement.Application/Extensions/General.cs +++ b/PostFundManagement.Application/Extensions/General.cs @@ -1,4 +1,4 @@ -namespace PostFundManagement.Application.Extensions; +namespace PostFundManagement.Application.Extensions; public static class General { @@ -16,7 +16,8 @@ public static class General public static (bool Decoded, Guid userId) GetUserId(this ClaimsPrincipal principal) { - var subClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value ?? principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; + var subClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value + ?? principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; var decoded = Guid.TryParse(subClaim, out var userId); @@ -25,20 +26,34 @@ public static class General public static (bool Decoded, string email) GetUserEmail(this ClaimsPrincipal principal) { - var emailClaim = principal.FindFirst("email")?.Value ?? principal.FindFirst(ClaimTypes.Email)?.Value; + var emailClaim = principal.FindFirst("email")?.Value + ?? principal.FindFirst(ClaimTypes.Email)?.Value + ?? principal.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value + ?? principal.FindFirst("preferred_username")?.Value + ?? principal.FindFirst("name")?.Value + ?? principal.FindFirst(ClaimTypes.Name)?.Value; var decoded = !string.IsNullOrWhiteSpace(emailClaim); - return (decoded, emailClaim!); + return (decoded, emailClaim ?? string.Empty); } public static (bool Decoded, string role) GetUserRole(this ClaimsPrincipal principal) { - var roleClaim = principal.FindFirst("role")?.Value ?? principal.FindFirst(ClaimTypes.Role)?.Value; + var roleClaims = principal.FindAll("role") + .Concat(principal.FindAll(ClaimTypes.Role)) + .Concat(principal.FindAll("http://schemas.microsoft.com/ws/2008/06/identity/claims/role")) + .Concat(principal.FindAll("roles")) + .Select(c => c.Value) + .Where(v => !string.IsNullOrWhiteSpace(v)) + .ToList(); - var decoded = !string.IsNullOrWhiteSpace(roleClaim); + var partnerRole = roleClaims.FirstOrDefault(r => r.Equals("Partner", StringComparison.OrdinalIgnoreCase)); + var role = partnerRole ?? roleClaims.FirstOrDefault(); - return (decoded, roleClaim!); + var decoded = !string.IsNullOrWhiteSpace(role); + + return (decoded, role ?? string.Empty); } public static Microsoft.AspNetCore.Mvc.ProblemDetails ToProblems(this IReadOnlyList errors, int statusCode = StatusCodes.Status400BadRequest) => new() diff --git a/PostFundManagement.PartnerUI/src/auth/AuthProvider.tsx b/PostFundManagement.PartnerUI/src/auth/AuthProvider.tsx index 6eee598..a933afc 100644 --- a/PostFundManagement.PartnerUI/src/auth/AuthProvider.tsx +++ b/PostFundManagement.PartnerUI/src/auth/AuthProvider.tsx @@ -17,7 +17,6 @@ import { AccountActivationModal } from '../components/auth/AccountActivationModa import { NonPartnerAccessDeniedModal } from '../components/auth/NonPartnerAccessDeniedModal'; import { SessionExpiredModal } from '../components/auth/SessionExpiredModal'; import { encodeHashIdSync } from '../services/toolsService'; -import { verifyTenantLockout, clearTenantLockoutCache } from '../services/tenantLockoutService'; import { onSessionExpired, triggerSessionExpired, @@ -510,35 +509,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => return; } - // Step 3: Verify tenant lockout using the HashId-encoded tenant ID - const effectiveTenantId = - parsedTenantId ?? - syncResult.data?.tenantId ?? - syncResult.data?.organisationId ?? - tenantClaimValues.tenantId; - - const tenantLockoutResult = await verifyTenantLockout(effectiveTenantId, tenantClaim); - - if (!tenantLockoutResult.isActive) { - setIsPendingActivation(true); - setIsSynced(false); - setActivationError(tenantLockoutResult.errorMessage || 'Tenant is inactive or not found'); - setIsLoading(false); - return; - } - - if (tenantLockoutResult.tenant?.name) { - profile.tenant = tenantLockoutResult.tenant.name; - setTenantName(tenantLockoutResult.tenant.name); - } - if (tenantLockoutResult.tenant?.id !== undefined && !profile.tenant_id) { - const numericTid = Number(tenantLockoutResult.tenant.id); - if (!isNaN(numericTid) && numericTid > 0) { - profile.tenant_id = numericTid; - setTenantId(numericTid); - } - } - setUser({ ...profile }); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); @@ -669,39 +639,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => return; } - // Step 3: Verify tenant lockout using HashId-encoded tenant ID - const effectiveTenantId = - storedTenantId ?? - syncResult.data?.tenantId ?? - syncResult.data?.organisationId ?? - tenantClaimValues.tenantId; - - const tenantLockoutResult = await verifyTenantLockout( - effectiveTenantId, - storedTenantClaim, - hadPendingActivation - ); - - if (!tenantLockoutResult.isActive) { - setIsPendingActivation(true); - setIsSynced(false); - setActivationError(tenantLockoutResult.errorMessage || 'Tenant is inactive or not found'); - return; - } - - if (tenantLockoutResult.tenant?.name) { - if (resolvedUser) { - resolvedUser.tenant = tenantLockoutResult.tenant.name; - } - setTenantName(tenantLockoutResult.tenant.name); - } - if (tenantLockoutResult.tenant?.id !== undefined && resolvedUser && !resolvedUser.tenant_id) { - const numericTid = Number(tenantLockoutResult.tenant.id); - if (!isNaN(numericTid) && numericTid > 0) { - resolvedUser.tenant_id = numericTid; - setTenantId(numericTid); - } - } if (resolvedUser) { setUser({ ...resolvedUser }); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(resolvedUser)); @@ -746,8 +683,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const syncUserSession = async (force = true): Promise => { try { await loadRuntimeConfig(); - clearTenantLockoutCache(); - if (sub) { + if (sub) { clearCachedUserRbac(sub); } @@ -793,41 +729,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); } catch {} - const tenantClaimValues = extractTenantClaimValues(claims, accessToken); - const effectiveTenantId = - currentTenantId ?? - result.data?.tenantId ?? - result.data?.organisationId ?? - tenantClaimValues.tenantId; - const effectiveTenantClaim = currentTenantClaim || tenantClaimValues.tenant || undefined; - - // Step 3: Verify tenant lockout using HashId-encoded tenant ID - const tenantLockoutResult = await verifyTenantLockout( - effectiveTenantId, - effectiveTenantClaim, - true - ); - if (!tenantLockoutResult.isActive) { - const msg = tenantLockoutResult.errorMessage || 'Tenant is not Active. Lockout active.'; - setIsPendingActivation(true); - setIsSynced(false); - setActivationError(msg); - return { - success: false, - isActivationPending: true, - errorMessage: msg, - }; - } - - if (tenantLockoutResult.tenant?.name) { - setTenantName(tenantLockoutResult.tenant.name); - } - if (tenantLockoutResult.tenant?.id !== undefined && !tenantId) { - const numericTid = Number(tenantLockoutResult.tenant.id); - if (!isNaN(numericTid) && numericTid > 0) { - setTenantId(numericTid); - } - } + // Partner user verified successfully setActivationPending(false); setIsPendingActivation(false); @@ -873,8 +775,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => localStorage.removeItem(LOCAL_STORAGE_SUB_KEY); localStorage.removeItem(LOCAL_STORAGE_CLAIMS_KEY); clearUserSyncInBrowser(); - clearTenantLockoutCache(); - setUser(null); + setUser(null); setSub(''); setClaims({}); setTenantId(null); diff --git a/PostFundManagement.PartnerUI/src/services/api/index.ts b/PostFundManagement.PartnerUI/src/services/api/index.ts index 7d36d91..1dabe7a 100644 --- a/PostFundManagement.PartnerUI/src/services/api/index.ts +++ b/PostFundManagement.PartnerUI/src/services/api/index.ts @@ -1585,13 +1585,7 @@ export const deleteUserClaim = async (targetUserId: string, claimId: string | nu await apiClient.delete(`/v1/api/users/${targetUserId}/claims/${claimId}`); }; -export const forceLogoutUser = async (userId: string): Promise => { - await apiClient.put(`/v1/api/users/${userId}/session/logout`); -}; - -export const setUserLockout = async (userId: string, disable: boolean): Promise => { - await apiClient.put(`/v1/api/users/${userId}/session/lockout/${disable}`); -}; +// Administrative functions (forceLogoutUser, setUserLockout) removed from PartnerUI import { syncUserAccount, diff --git a/PostFundManagement.PartnerUI/src/services/userSyncService.ts b/PostFundManagement.PartnerUI/src/services/userSyncService.ts index 36617ec..591b332 100644 --- a/PostFundManagement.PartnerUI/src/services/userSyncService.ts +++ b/PostFundManagement.PartnerUI/src/services/userSyncService.ts @@ -189,19 +189,7 @@ async function executeBackendSync( userSub?: string, profile?: UserSyncProfile ): Promise<{ endpoint: string; data?: any }> { - const rawTenantId = profile?.tenant_id ?? (profile as any)?.tenantId; - const parsedTenantId = - rawTenantId !== undefined && rawTenantId !== null && rawTenantId !== '' - ? Number(rawTenantId) - : undefined; - - const resolvedOrgId = - parsedTenantId !== undefined - ? parsedTenantId - : profile?.organisationId !== undefined && profile?.organisationId !== null && profile?.organisationId !== '' - ? Number(profile.organisationId) - : undefined; - + const endpoint = '/v1/api/integration/sync'; const payload = { id: userSub || profile?.id || profile?.sub, sub: userSub || profile?.sub, @@ -209,40 +197,25 @@ async function executeBackendSync( name: profile?.name || undefined, role: profile?.role || undefined, tenant: profile?.tenant || undefined, - tenant_id: parsedTenantId, - organisationId: resolvedOrgId, + tenant_id: profile?.tenant_id, + organisationId: profile?.organisationId, }; - let lastError: any = null; - - for (const endpoint of USER_SYNC_CANDIDATE_ENDPOINTS) { - try { - let res; - if (endpoint === '/v1/api/integration/sync') { - try { - res = await apiClient.post(endpoint, payload); - } catch (postErr: any) { - const errStatus = postErr?.response?.status; - if (errStatus === 400 || errStatus === 415 || errStatus === 422) { - res = await apiClient.post(endpoint, {}); - } else { - throw postErr; - } - } - } else { - res = await apiClient.post(endpoint, payload); - } - return { endpoint, data: res?.data }; - } catch (err: any) { - lastError = err; - const status = err?.response?.status; - if (status === 401 || status === 403 || status === 423) { + try { + const res = await apiClient.post(endpoint, payload); + return { endpoint, data: res?.data }; + } catch (err: any) { + const status = err?.response?.status; + if (status === 400 || status === 415 || status === 422) { + try { + const resFallback = await apiClient.post(endpoint, {}); + return { endpoint, data: resFallback?.data }; + } catch (fallbackErr) { throw err; } } + throw err; } - - throw lastError || new Error('Failed to reach user sync endpoint'); } export async function syncUserAccount( -- 2.47.3 From 16fb6b94349d8033e27d0f3772f218474267d46c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 24 Oct 2026 11:48:19 +0200 Subject: [PATCH 2/2] Simplified lockout modals --- PostFundManagement.PartnerUI/index.html | 6 +- .../auth/AccountActivationModal.tsx | 218 ++++++------------ .../auth/NonPartnerAccessDeniedModal.tsx | 176 +++++--------- .../components/auth/SessionExpiredModal.tsx | 99 +++----- PostFundManagement.PartnerUI/src/index.css | 8 +- 5 files changed, 168 insertions(+), 339 deletions(-) diff --git a/PostFundManagement.PartnerUI/index.html b/PostFundManagement.PartnerUI/index.html index f266ad6..0f20073 100644 --- a/PostFundManagement.PartnerUI/index.html +++ b/PostFundManagement.PartnerUI/index.html @@ -1,5 +1,5 @@ - + @@ -10,8 +10,8 @@ - -
+ +
diff --git a/PostFundManagement.PartnerUI/src/components/auth/AccountActivationModal.tsx b/PostFundManagement.PartnerUI/src/components/auth/AccountActivationModal.tsx index 7c86149..011d00c 100644 --- a/PostFundManagement.PartnerUI/src/components/auth/AccountActivationModal.tsx +++ b/PostFundManagement.PartnerUI/src/components/auth/AccountActivationModal.tsx @@ -2,18 +2,14 @@ import React, { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { - ShieldAlert, RefreshCw, LogOut, - UserCheck, - CheckCircle2, - AlertTriangle, - ArrowLeft, - Mail, Building2, - Fingerprint, + Clock, + ArrowLeft, + Sparkles, } from 'lucide-react'; -import { BrandLogo } from '../ui/BrandLogo'; +import { PfmPartnerLogo } from '../common/PfmPartnerLogo'; import { AuthUserProfile, UserClaims } from '../../auth/AuthProvider'; import { cancelSessionExpiration, dismissSubordinateModals } from '../../services/sessionExpiration'; @@ -29,7 +25,6 @@ interface AccountActivationModalProps { export const AccountActivationModal: React.FC = ({ user, claims, - sub, onSync, onLogout, initialError, @@ -37,7 +32,6 @@ export const AccountActivationModal: React.FC = ({ const navigate = useNavigate(); const [isSyncing, setIsSyncing] = useState(false); const [errorMessage, setErrorMessage] = useState(initialError || null); - const [syncAttemptCount, setSyncAttemptCount] = useState(0); useEffect(() => { if (initialError !== undefined) { @@ -54,16 +48,9 @@ export const AccountActivationModal: React.FC = ({ user?.name || (claims.name as string) || (claims.preferred_username as string) || - 'Registered Partner'; - - const displayEmail = - user?.email || - (claims.email as string) || - (claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] as string) || - ''; + 'Partner'; const tenant = user?.tenant || (claims.tenant as string) || (claims.org as string) || 'Partner Organisation'; - const role = user?.role || (claims.role as string) || 'Partner'; const handleSyncClick = async () => { if (isSyncing) return; @@ -80,10 +67,9 @@ export const AccountActivationModal: React.FC = ({ } if (!result.success) { - setSyncAttemptCount((prev) => prev + 1); setErrorMessage( result.errorMessage || - 'Your partner account is not yet enabled. Please ask your funding administrator to activate your account.' + 'Activation is still being reviewed by your funding administrator. Please check back shortly.' ); } } catch (err: unknown) { @@ -93,10 +79,7 @@ export const AccountActivationModal: React.FC = ({ navigate('/', { replace: true }); return; } - setErrorMessage( - errObj?.message || - 'Account activation check encountered an error. Please confirm with your administrator and try again.' - ); + setErrorMessage('Unable to verify activation right now. Please try again in a few moments.'); } finally { setIsSyncing(false); } @@ -116,147 +99,87 @@ export const AccountActivationModal: React.FC = ({ 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 sm:p-6 bg-slate-950/80 backdrop-blur-2xl animate-in fade-in duration-300 select-none overflow-y-auto" > + {/* Background ambient lighting matching LandingPage */} +
+
+
+
e.stopPropagation()} > -
+ {/* Glowing Brand Icon Lockup */} +
+
+ +
-
-
-
-
- - - - - -
-
- - Access Restricted • Activation Pending - -

- {errorMessage?.toLowerCase().includes('tenant') - ? 'Organisation Account Inactive' - : 'Partner Account Pending Activation'} -

-
-
- - + {/* Title & Organization Pill */} +
+
+ + Activation Pending +
+

+ Welcome,{' '} + + {displayName} + +

+
+ + {tenant}
-
-
-
- - Signed In Successfully -
-

- You have signed in successfully, but your partner account is waiting to be activated by the funding administrator. -

-

- Once your administrator confirms your account is active, click{' '} - Check Account Status below to open your dashboard. -

-
- -
-
- Your Partner Profile - - Pending Activation - -
- -
-
- -
- Name - {displayName} -
-
- -
- -
- Email - {displayEmail || 'Not specified'} -
-
- -
- -
- Organisation / Role - - {tenant} ({role}) - -
-
- -
- -
- Account ID - {sub || 'SSO-Principal'} -
-
-
-
- - {errorMessage && ( -
- -
-

Account Not Activated Yet

-

{errorMessage}

- {syncAttemptCount > 0 && ( -

- Checked #{syncAttemptCount} at {new Date().toLocaleTimeString()} -

- )} -
-
- )} -
- -
-
- - - + {/* Core Friendly Message */} +

+ Your credentials have been authenticated. Your funding administrator is finalizing your portfolio access. +

+ + {/* Notice badge if check returned not yet ready */} + {errorMessage && ( +
+ + {errorMessage}
+ )} + {/* Action Button matching Specular / Landing styling */} +
+ + {/* Minimal Secondary Actions */} +
+ + • + +
@@ -266,4 +189,3 @@ export const AccountActivationModal: React.FC = ({ ? createPortal(modalContent, document.body) : modalContent; }; - diff --git a/PostFundManagement.PartnerUI/src/components/auth/NonPartnerAccessDeniedModal.tsx b/PostFundManagement.PartnerUI/src/components/auth/NonPartnerAccessDeniedModal.tsx index cf67728..5c44393 100644 --- a/PostFundManagement.PartnerUI/src/components/auth/NonPartnerAccessDeniedModal.tsx +++ b/PostFundManagement.PartnerUI/src/components/auth/NonPartnerAccessDeniedModal.tsx @@ -1,16 +1,8 @@ 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 { ShieldCheck, LogOut, ArrowLeft, Lock } from 'lucide-react'; +import { PfmPartnerLogo } from '../common/PfmPartnerLogo'; import { AuthUserProfile, UserClaims } from '../../auth/AuthProvider'; import { cancelSessionExpiration, dismissSubordinateModals } from '../../services/sessionExpiration'; @@ -22,14 +14,9 @@ interface NonPartnerAccessDeniedModalProps { onLogout: () => Promise; } -/** - * 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 = ({ user, claims, - sub, roles, onLogout, }) => { @@ -40,133 +27,79 @@ export const NonPartnerAccessDeniedModal: React.FC 0 ? roles.join(', ') : user?.role || (claims.role as string) || 'Unassigned'; + const assignedRolesDisplay = roles.length > 0 ? roles.join(', ') : user?.role || (claims.role as string) || 'Standard User'; const handleLogoutClick = async () => { await onLogout(); navigate('/', { replace: true }); }; + const handleLandingClick = () => { + navigate('/'); + }; + const modalContent = (
-
-
+
+
+
-
-
-
- -
-
-
- - - Partner Role Required - - Security Policy -
-

- Access Denied: Partner Accounts Only -

-

- This application is exclusively reserved for users assigned the Partner role. Your account role ({assignedRolesDisplay}) is not permitted on the Partner Portal. -

-
-
- -
-
- Account Name - {displayName} -
- - {displayEmail && ( -
- - - Email - - {displayEmail} -
- )} - -
- - - Assigned Role(s) - - - {assignedRolesDisplay} - -
- - {tenant && ( -
- - - Organisation / Tenant - - {tenant} -
- )} - - {sub && ( -
- - - Subject Key - - {sub} -
- )} -
- -
-

- - Portal Separation Notice -

-

- Internal staff, executives, programme managers, and administrators must sign in through the PFM Management Console. Only external grant recipients and project partners with the Partner role are permitted to use this portal. -

-
- -
- -
+
e.stopPropagation()} + > +
+
+
-
-
- - PFM Partner Portal +
+
+ + Partner Access Only
- Role Security Gate +

+ Role Authorization Required +

+
+ +

+ This portal is reserved for registered Grantee and Partner accounts. Your current profile ({assignedRolesDisplay}) does not have access. +

+ +
+ Signed in as {displayEmail} +
+ +
+ + +
@@ -176,4 +109,3 @@ export const NonPartnerAccessDeniedModal: React.FC = ({ const modalContent = (
setIsPaused(true)} onMouseLeave={() => setIsPaused(false)} role="dialog" aria-modal="true" aria-labelledby="session-expired-title" > +
+
+
+
-
- -
-
-
- -
-
-
-

- Session Expired -

- - HTTP 401 - -
-

Please sign in again to continue

-
-
- - -
- -
-

- Your active working session has ended for security. To protect your information, please return to the home page and sign in again. -

- {reason && ( -
- - {reason} -
- )} +
+
+
-
- - - Returning to home page in{' '} - {secondsRemaining}s - - {isPaused && ( - - Paused on hover - - )} +
+ + Session Expired
+

+ Security Session Ended +

+
-
+

+ {reason || 'Your security credentials have expired for your protection. Please sign in again to continue.'} +

+ + {/* Smooth Countdown Bar */} +
+
+ + Redirecting in {secondsRemaining}s +
-
+
@@ -179,4 +149,3 @@ export const SessionExpiredModal: React.FC = ({ ? createPortal(modalContent, document.body) : modalContent; }; - diff --git a/PostFundManagement.PartnerUI/src/index.css b/PostFundManagement.PartnerUI/src/index.css index 1d89e5d..87916d6 100644 --- a/PostFundManagement.PartnerUI/src/index.css +++ b/PostFundManagement.PartnerUI/src/index.css @@ -1,7 +1,13 @@ @import "tailwindcss"; +:root { + color-scheme: dark; +} + @layer base { - body { + html, body { + background-color: #020617; /* slate-950 */ + color: #f8fafc; /* slate-100 */ font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } -- 2.47.3