import { getTenant, getTenants } from './api'; import { ActivityStatus, Tenant } from '../types'; import { setActivationPending } from './userSyncService'; import { encodeHashIdSync, isAlreadyHashId } from './toolsService'; export interface TenantLockoutResult { isActive: boolean; tenant: Tenant | null; errorMessage?: string; statusCode?: number; } let cachedTenantLockout: { key: string; result: TenantLockoutResult; timestamp: number; } | null = null; const LOCKOUT_CACHE_TTL_MS = 60 * 1000; export function clearTenantLockoutCache(): void { cachedTenantLockout = null; } export function isStatusActive(status: any): boolean { if (status === undefined || status === null || status === '') { return false; } if (status === ActivityStatus.Active || status === 1 || status === '1') { return true; } if (typeof status === 'string' && status.trim().toLowerCase() === 'active') { return true; } if (status === true) { return true; } return false; } export function getStatusLabel(status: any): string { if (status === ActivityStatus.Active || status === 1 || String(status).toLowerCase() === 'active') { return 'Active'; } if (status === ActivityStatus.Inactive || status === 2 || String(status).toLowerCase() === 'inactive') { return 'Inactive'; } if (status === ActivityStatus.Disabled || status === 3 || String(status).toLowerCase() === 'disabled') { return 'Disabled'; } if (String(status).toLowerCase() === 'suspended') { return 'Suspended'; } if (status === 0 || String(status).toLowerCase() === 'pending') { return 'Pending'; } if (status !== undefined && status !== null && status !== '') { return String(status); } return 'Not Specified'; } function isValidHashableOrHashedTenantId(val: unknown): val is number | string { if (val === undefined || val === null || val === '') return false; if (typeof val === 'number') { return !isNaN(val) && isFinite(val) && val > 0; } if (typeof val === 'string') { const trimmed = val.trim(); if (!trimmed) return false; if (/^\d+$/.test(trimmed)) { return parseInt(trimmed, 10) > 0; } return isAlreadyHashId(trimmed); } return false; } export async function verifyTenantLockout( tenantId?: number | string | null, tenantClaim?: string | null, forceRefresh = false ): Promise { // Resolve a valid numeric ID or HashId to avoid passing plain-text tenant names to /v1/api/governance/tenants/{id} let candidateId: number | string | null = null; if (isValidHashableOrHashedTenantId(tenantId)) { candidateId = tenantId; } else if (isValidHashableOrHashedTenantId(tenantClaim)) { candidateId = tenantClaim; } const cacheKey = String(candidateId ?? tenantClaim ?? 'default-1').trim().toLowerCase(); if (!forceRefresh && cachedTenantLockout && cachedTenantLockout.key === cacheKey) { if ( cachedTenantLockout.result.isActive && Date.now() - cachedTenantLockout.timestamp < LOCKOUT_CACHE_TTL_MS ) { return cachedTenantLockout.result; } } let tenantRes: Tenant | null = null; let lastError: any = null; // 1. If we have a valid numeric tenantId or HashId, encode it via HashIds and call GET /v1/api/governance/tenants/{hashedId} if (candidateId !== null) { try { const hashedId = encodeHashIdSync(candidateId); tenantRes = await getTenant(hashedId || candidateId); } catch (err: any) { lastError = err; tenantRes = null; } } // 2. If tenant lookup by ID wasn't possible or didn't resolve, resolve via getTenants() (matching tenantClaim name if provided) if (!tenantRes) { try { const allTenants = await getTenants(); if (Array.isArray(allTenants) && allTenants.length > 0) { const claimStr = tenantClaim ? String(tenantClaim).trim().toLowerCase() : ''; const matched = claimStr ? allTenants.find( (t) => (t.name && t.name.trim().toLowerCase() === claimStr) || String(t.id).toLowerCase() === claimStr || encodeHashIdSync(t.id).toLowerCase() === claimStr ) : null; const targetTenant = matched || allTenants.find((t) => isStatusActive((t as any).status ?? (t as any).Status)) || allTenants[0]; if (targetTenant && targetTenant.id !== undefined && targetTenant.id !== null) { try { const hashedTargetId = encodeHashIdSync(targetTenant.id); tenantRes = await getTenant(hashedTargetId || targetTenant.id); lastError = null; } catch { tenantRes = targetTenant; lastError = null; } } } } catch (err: any) { if (!lastError) lastError = err; } } // 3. Final fallback: query default primary tenant ID 1 encoded as a HashId (matching ConsoleUI) if (!tenantRes && candidateId !== 1 && candidateId !== '1') { try { const hashedDefaultId = encodeHashIdSync(1); tenantRes = await getTenant(hashedDefaultId); lastError = null; } catch (err: any) { if (!lastError) lastError = err; tenantRes = null; } } if (!tenantRes || (typeof tenantRes === 'object' && Object.keys(tenantRes).length === 0)) { let serverMsg = lastError?.response?.data?.detail || lastError?.response?.data?.message || lastError?.message || 'No tenant record returned'; if (typeof serverMsg === 'string') { if (/^network\s*error$/i.test(serverMsg.trim())) { serverMsg = 'Permission Check'; } else { serverMsg = serverMsg.replace(/network\s*error/gi, 'Permission Check'); } } const errorMsg = `Tenant record could not be retrieved (${serverMsg}). Account is locked out.`; setActivationPending(true, errorMsg); return { isActive: false, tenant: null, errorMessage: errorMsg, statusCode: lastError?.response?.status, }; } const tenantObj: Tenant = (tenantRes as any)?.data || (tenantRes as any)?.result || tenantRes; const rawStatus = (tenantObj as any)?.status ?? (tenantObj as any)?.Status ?? (tenantObj as any)?.activityStatus ?? (tenantObj as any)?.ActivityStatus; const active = isStatusActive(rawStatus); if (!active) { const statusLabel = getStatusLabel(rawStatus); const tenantName = tenantObj?.name || String(candidateId ?? tenantClaim ?? '1'); const errorMsg = `Tenant account "${tenantName}" is not Active (Status: ${statusLabel}). Account is locked out.`; setActivationPending(true, errorMsg); return { isActive: false, tenant: tenantObj, errorMessage: errorMsg, }; } setActivationPending(false); const successResult: TenantLockoutResult = { isActive: true, tenant: tenantObj, }; cachedTenantLockout = { key: cacheKey, result: successResult, timestamp: Date.now(), }; return successResult; }