diff --git a/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx b/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx index 5045b61..75e4c20 100644 --- a/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx +++ b/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx @@ -10,13 +10,14 @@ import { isActivationPending, getActivationPendingReason, clearUserSyncInBrowser, + setActivationPending, UserSyncResult, } from '../services/userSyncService'; import { AccountActivationModal } from '../components/auth/AccountActivationModal'; import { PartnerAccessDeniedModal } from '../components/auth/PartnerAccessDeniedModal'; import { SessionExpiredModal } from '../components/auth/SessionExpiredModal'; -import { encodeHashIdSync } from '../services/toolsService'; -import { verifyTenantLockout } from '../services/tenantLockoutService'; +import { encodeHashIdSync } from '../utils/hashids'; +import { verifyTenantLockout, clearTenantLockoutCache } from '../services/tenantLockoutService'; import { onSessionExpired, triggerSessionExpired, @@ -307,11 +308,6 @@ export function resetUserManager(): void { userManagerInstance = null; } -// Automatically reset UserManager when runtime configuration is updated -onConfigChanged(() => { - resetUserManager(); -}); - export function getUserManager(): UserManager { if (!userManagerInstance && typeof window !== 'undefined') { const sec = getSecuritySettings(); @@ -437,8 +433,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(mergedClaims)); const tenantClaimValues = extractTenantClaimValues(mergedClaims, token); - const tenantClaim = (mergedClaims.tenant as string) || (oidcUser.profile?.tenant as string) || tenantClaimValues.tenant || undefined; - const parsedTenantId = extractTenantId(mergedClaims, token); + let tenantClaim = (mergedClaims.tenant as string) || (oidcUser.profile?.tenant as string) || tenantClaimValues.tenant || undefined; + let parsedTenantId = extractTenantId(mergedClaims, token); setTenantId(parsedTenantId); const profile: AuthUserProfile = { @@ -458,29 +454,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => // Clean up code and state from URL window.history.replaceState({}, document.title, window.location.pathname); - // Verify tenant lockout at the instance tenant and tenant_id claims are retrieved - const tenantLockoutResult = await verifyTenantLockout( - parsedTenantId ?? tenantClaimValues.tenantId, - 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); - localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); - } else if (tenantClaim) { - setTenantName(tenantClaim); - } - - // Synchronize authenticated user profile with backend directory + // Step 1: Synchronize authenticated user profile with backend directory FIRST (/v1/api/integration/sync) try { const syncResult = await syncUserAccount(userSub, token, profile, true); if (syncResult.isUnauthorized) { @@ -488,6 +462,51 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => window.location.replace('/'); return; } + + // Step 2: Retrieve user roles, role claims, and user claims (Requirement 2 & 3 & 7) + try { + const resolved = await resolveUserRolesAndClaims(userSub, mergedClaims, profile); + if (resolved.primaryRole) { + profile.role = resolved.primaryRole; + } + if (resolved.tenant) { + profile.tenant = resolved.tenant; + tenantClaim = resolved.tenant; + setTenantName(resolved.tenant); + } + if (resolved.tenant_id !== undefined) { + profile.tenant_id = resolved.tenant_id; + parsedTenantId = resolved.tenant_id; + setTenantId(resolved.tenant_id); + } + setUser({ ...profile }); + setClaims({ ...resolved.claims }); + localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); + localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); + } catch {} + + // Step 3: Verify tenant lockout now that user is synced and authenticated + const tenantLockoutResult = await verifyTenantLockout( + parsedTenantId ?? tenantClaimValues.tenantId, + 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); + localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); + } else if (tenantClaim) { + setTenantName(tenantClaim); + } + if (syncResult.isActivationPending) { setIsPendingActivation(true); setIsSynced(false); @@ -496,26 +515,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => setIsPendingActivation(false); setIsSynced(true); setActivationError(null); - - // Retrieve user roles, role claims, and user claims (Requirement 2 & 3 & 7) - try { - const resolved = await resolveUserRolesAndClaims(userSub, mergedClaims, profile); - if (resolved.primaryRole) { - profile.role = resolved.primaryRole; - } - if (resolved.tenant) { - profile.tenant = resolved.tenant; - setTenantName(resolved.tenant); - } - if (resolved.tenant_id !== undefined) { - profile.tenant_id = resolved.tenant_id; - setTenantId(resolved.tenant_id); - } - setUser({ ...profile }); - setClaims({ ...resolved.claims }); - localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); - localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); - } catch {} } } catch (syncErr: any) { if (syncErr?.response?.status === 401) { @@ -525,7 +524,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => } setIsPendingActivation(true); setIsSynced(false); - setActivationError(syncErr?.message || 'Account activation pending'); + const rawMsg = syncErr?.message || 'Account activation pending'; + const cleanMsg = /network\s*error/i.test(rawMsg) ? rawMsg.replace(/network\s*error/gi, 'Permission Check') : rawMsg; + setActivationError(cleanMsg); } setIsLoading(false); @@ -561,10 +562,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => setSub(storedSub); } - const storedTenantId = extractTenantId(parsedClaims, storedToken); + let storedTenantId = extractTenantId(parsedClaims, storedToken); setTenantId(storedTenantId); const tenantClaimValues = extractTenantClaimValues(parsedClaims, storedToken); - const storedTenantClaim = (parsedClaims.tenant as string) || tenantClaimValues.tenant || undefined; + let storedTenantClaim = (parsedClaims.tenant as string) || tenantClaimValues.tenant || undefined; let resolvedUser: AuthUserProfile | null = null; if (storedUser) { @@ -588,9 +589,53 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => // Initial fast unblock: valid persisted session found, unblock UI immediately setIsLoading(false); - // Asynchronous background revalidation (Tenant Lockout, User Sync, Role Claims) + // Asynchronous background revalidation (User Sync, Role Claims, Tenant Lockout) (async () => { try { + const wasPending = isActivationPending(); + setIsPendingActivation(wasPending); + setActivationError(getActivationPendingReason()); + + // Step 1: Sync user account FIRST + const syncResult = await syncUserAccount( + storedSub || '', + storedToken, + resolvedUser || undefined, + false + ); + + if (syncResult.isUnauthorized) { + await logout(); + return; + } + + // Step 2: Retrieve user roles, role claims, and user claims + try { + const resolved = await resolveUserRolesAndClaims( + storedSub || '', + parsedClaims, + resolvedUser || {} + ); + if (resolvedUser) { + if (resolved.primaryRole) resolvedUser.role = resolved.primaryRole; + if (resolved.tenant) { + resolvedUser.tenant = resolved.tenant; + storedTenantClaim = resolved.tenant; + setTenantName(resolved.tenant); + } + if (resolved.tenant_id !== undefined) { + resolvedUser.tenant_id = resolved.tenant_id; + storedTenantId = resolved.tenant_id; + setTenantId(resolved.tenant_id); + } + setUser({ ...resolvedUser }); + localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(resolvedUser)); + } + setClaims({ ...resolved.claims }); + localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); + } catch {} + + // Step 3: Verify tenant lockout now that user is verified active const tenantLockoutResult = await verifyTenantLockout( storedTenantId ?? tenantClaimValues.tenantId, storedTenantClaim @@ -612,22 +657,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => setTenantName(tenantLockoutResult.tenant.name); } - const wasPending = isActivationPending(); - setIsPendingActivation(wasPending); - setActivationError(getActivationPendingReason()); - - const syncResult = await syncUserAccount( - storedSub || '', - storedToken, - resolvedUser || undefined, - false - ); - - if (syncResult.isUnauthorized) { - await logout(); - return; - } - if (syncResult.isActivationPending) { setIsPendingActivation(true); setIsSynced(false); @@ -639,27 +668,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => setIsPendingActivation(false); setIsSynced(true); setActivationError(null); - - const resolved = await resolveUserRolesAndClaims( - storedSub || '', - parsedClaims, - resolvedUser || {} - ); - if (resolvedUser) { - if (resolved.primaryRole) resolvedUser.role = resolved.primaryRole; - if (resolved.tenant) { - resolvedUser.tenant = resolved.tenant; - setTenantName(resolved.tenant); - } - if (resolved.tenant_id !== undefined) { - resolvedUser.tenant_id = resolved.tenant_id; - setTenantId(resolved.tenant_id); - } - setUser({ ...resolvedUser }); - localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(resolvedUser)); - } - setClaims({ ...resolved.claims }); - localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); } } catch (syncErr: any) { if (syncErr?.response?.status === 401) { @@ -668,7 +676,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => } setIsPendingActivation(true); setIsSynced(false); - setActivationError(syncErr?.message || 'Account activation pending'); + const rawMsg = syncErr?.message || 'Account activation pending'; + const cleanMsg = /network\s*error/i.test(rawMsg) ? rawMsg.replace(/network\s*error/gi, 'Permission Check') : rawMsg; + setActivationError(cleanMsg); } })(); @@ -687,12 +697,65 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const syncUserSession = async (force = true): Promise => { try { - // Re-verify tenant status via GetTenant as part of the lockout resolution - const tenantClaimValues = extractTenantClaimValues(claims, accessToken); - const currentTenantId = tenantId ?? tenantClaimValues.tenantId; - const currentTenantClaim = (claims.tenant as string) || user?.tenant || tenantClaimValues.tenant || undefined; + await loadRuntimeConfig(); + clearTenantLockoutCache(); - const tenantLockoutResult = await verifyTenantLockout(currentTenantId, currentTenantClaim); + // Step 1: Proceed with user sync FIRST (/v1/api/integration/sync) + const result = await syncUserAccount(sub, accessToken, user || undefined, force); + if (result.isUnauthorized) { + await logout(); + return result; + } + if (result.isActivationPending) { + setIsPendingActivation(true); + setIsSynced(false); + setActivationError(result.errorMessage || null); + return result; + } + + // Step 2: Refresh user roles and claims from backend + let currentTenantId: number | string | null = tenantId; + let currentTenantClaim: string | undefined = + (claims.tenant as string) || user?.tenant || undefined; + + try { + const resolved = await resolveUserRolesAndClaims(sub, claims, user || {}); + const updatedProfile = user ? { ...user } : null; + if (updatedProfile && resolved.primaryRole) { + updatedProfile.role = resolved.primaryRole; + } + if (resolved.tenant) { + currentTenantClaim = resolved.tenant; + setTenantName(resolved.tenant); + if (updatedProfile) updatedProfile.tenant = resolved.tenant; + } + if (resolved.tenant_id !== undefined) { + currentTenantId = resolved.tenant_id; + setTenantId(resolved.tenant_id); + if (updatedProfile) updatedProfile.tenant_id = resolved.tenant_id; + } + if (updatedProfile) { + setUser(updatedProfile); + localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(updatedProfile)); + } + setClaims({ ...resolved.claims }); + localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); + } catch {} + + // Step 3: Re-verify tenant status via GetTenant now that user is synced + const tenantClaimValues = extractTenantClaimValues(claims, accessToken); + const effectiveTenantId = + currentTenantId ?? + result.data?.tenantId ?? + result.data?.organisationId ?? + tenantClaimValues.tenantId; + const effectiveTenantClaim = currentTenantClaim || tenantClaimValues.tenant || undefined; + + const tenantLockoutResult = await verifyTenantLockout( + effectiveTenantId, + effectiveTenantClaim, + true + ); if (!tenantLockoutResult.isActive) { const msg = tenantLockoutResult.errorMessage || 'Tenant is not Active. Lockout active.'; setIsPendingActivation(true); @@ -705,28 +768,22 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => }; } - // Tenant is verified Active: proceed with user sync - const result = await syncUserAccount(sub, accessToken, user || undefined, force); - if (result.isUnauthorized) { - await logout(); - return result; - } - if (result.success) { - setIsPendingActivation(false); - setIsSynced(true); - setActivationError(null); - } else if (result.isActivationPending) { - setIsPendingActivation(true); - setIsSynced(false); - setActivationError(result.errorMessage || null); + if (tenantLockoutResult.tenant?.name) { + setTenantName(tenantLockoutResult.tenant.name); } + + setActivationPending(false); + setIsPendingActivation(false); + setIsSynced(true); + setActivationError(null); return result; } catch (err: any) { if (err?.response?.status === 401) { await logout(); return { success: false, isUnauthorized: true }; } - const msg = err?.message || 'Sync failed'; + const rawMsg = err?.message || 'Sync failed'; + const msg = /network\s*error/i.test(rawMsg) ? rawMsg.replace(/network\s*error/gi, 'Permission Check') : rawMsg; setIsPendingActivation(true); setIsSynced(false); setActivationError(msg); diff --git a/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx index ec7dab9..afdc2fd 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx @@ -115,141 +115,94 @@ export const AccountActivationModal: React.FC = ({ navigate('/'); }; + const isTenantIssue = !!errorMessage?.toLowerCase().includes('tenant'); + const modalContent = (
- {/* Non-dismissable Card Container: No X button, no escape dismiss, no backdrop click */}
e.stopPropagation()} > - {/* Amber accent top bar */} -
+ {/* Subtle accent top border */} +
- {/* Modal Header */} -
+ {/* Header */} +
-
-
- - - - - +
+
+
- - Access Restricted • Lockout Active - -

- {errorMessage?.toLowerCase().includes('tenant') - ? 'Tenant Lockout • Account Inactive' - : 'Account Pending Administrator Activation'} +

+ {isTenantIssue ? 'Tenant Access Restricted' : 'Account Pending Activation'}

+

+ {isTenantIssue + ? 'The assigned tenant partition is currently inactive or restricted.' + : 'Your account is authenticated and awaiting administrator enablement.'} +

- +
- {/* Modal Body */} -
- {/* Status notification block */} -
-
- - Identity Verified & Signed In Successfully + {/* Body */} +
+ {/* User Profile Summary */} +
+
+
+ User + {displayName} +
+
+ Email + {displayEmail || '—'} +
-

- 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. -

-

- If your administrator informs you that your account has been activated, please click the{' '} - Sync Account button below to refresh your status and unlock the system. -

-
- - {/* User Profile Card */} -
-
- Authenticated User Profile - - Lockout Active - -
- -
-
- -
- Name - {displayName} -
+
+
+ Tenant + {tenant}
- -
- -
- Email - {displayEmail || 'Not specified'} -
-
- -
- -
- Tenant / Role - - {tenant} ({role}) - -
-
- -
- -
- Subject ID - {sub || 'SSO-Principal'} -
+
+ Role + {role}
- {/* Sync Attempt Feedback / Error Notice */} + {/* Feedback notice if sync failed */} {errorMessage && ( -
- -
-

- {errorMessage?.toLowerCase().includes('tenant') - ? 'Tenant Lockout In Effect' - : 'Account Not Activated Yet'} -

-

{errorMessage}

- {syncAttemptCount > 0 && ( -

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

- )} +
+ +
+ Notice: +

{errorMessage}

)} + +

+ Once your administrator enables your account, click Sync Account to enter the console. +

- {/* Modal Actions */} -
+ {/* Footer Actions */} +
diff --git a/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx index ce7eb0a..02f283f 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx @@ -61,114 +61,57 @@ export const PartnerAccessDeniedModal: React.FC = 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" > -
- {/* Glow Header Accent */} -
+
+
-
- {/* Header */} -
-
- +
+
+
+
-
-
- - - Restricted Role - - Security Policy -
-

- Access Denied: Partner Role +
+

+ Partner Portal Required

-

- Users assigned the Partner role by security are not permitted to access the PFM Backoffice Console. +

+ Accounts with the Partner role must access the Partner Portal.

- {/* User & Identity Summary Card */} -
-
- Account Name - {displayName} +
+
+ User + {displayName}
- {displayEmail && ( -
- - - Email - - {displayEmail} +
+ Email + {displayEmail}
)} - -
- - - Assigned Role - - - {role} - +
+ Role + {role}
- - {tenant && ( -
- - - Tenant Context - - {tenant} -
- )} - - {sub && ( -
- - - Subject Key - - {sub} -
- )}
- {/* Security Policy Information */} -
-

- - Administrative Boundary Notice -

-

- 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. -

-
+

+ The Console is reserved for statutory administrators and programme managers. +

- {/* Actions */} -
+
- - {/* Footer Brand watermark */} -
-
- - Post Fund Management (PFM) -
- National Security Gateway -
); diff --git a/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx index 7681a5a..b5dc830 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx @@ -87,7 +87,7 @@ export const SessionExpiredModal: React.FC = ({ const modalContent = (
setIsPaused(true)} onMouseLeave={() => setIsPaused(false)} role="dialog" @@ -96,27 +96,20 @@ export const SessionExpiredModal: React.FC = ({ >
- {/* Subtle decorative top accent bar */} -
+
- {/* Top bar with close button */}
-
- +
+
-
-

- Session Expired -

- - HTTP 401 - -
-

Authorization token invalidated

+

+ Session Expired +

+

Authorization credentials expired

@@ -125,60 +118,46 @@ export const SessionExpiredModal: React.FC = ({ 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" >
- {/* Informative Explanation (non-technical and clear) */} -
+

- 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.

{reason && ( -
+
{reason}
)}
- {/* Progress Bar Countdown */} -
-
- - - Returning to landing page in{' '} - {secondsRemaining}s +
+ + + Redirecting in {secondsRemaining}s + + {isPaused && ( + + Paused - {isPaused && ( - - Paused on hover - - )} -
- - {/* Progress bar line */} -
-
-
+ )}
- {/* Actions */} -
+
diff --git a/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx b/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx index f9d299d..cc0b558 100644 --- a/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx +++ b/PostFundManagement.ConsoleUI/src/components/monitoring/ContractsManagement.tsx @@ -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'; diff --git a/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx b/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx index 577cf4f..a919e50 100644 --- a/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx @@ -290,18 +290,18 @@ export const UserSecurityModal: React.FC = ({
{/* Navigation Tabs */} -
+
)} - {/* TAB 4: Sessions & Direct Account Interventions */} + {/* TAB 4: Sessions & Security Interventions */} {activeTab === 'sessions' && (
{/* Force Logout Action */} -
+

Revoke Active User Session -

- Terminates all active JWT tokens and sessions immediately without direct database intervention. + Immediately invalidates active JWT tokens and logs the user out.

{/* Lock / Unlock Account */} -
+

- Account Lockout Control - + Account Lockout

- 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.

-
+