Fixed sync handshake
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
isUserSyncedInBrowser,
|
||||
isActivationPending,
|
||||
getActivationPendingReason,
|
||||
setActivationPending,
|
||||
clearUserSyncInBrowser,
|
||||
UserSyncResult,
|
||||
} from '../services/userSyncService';
|
||||
@@ -16,7 +17,7 @@ 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 } from '../services/tenantLockoutService';
|
||||
import { verifyTenantLockout, clearTenantLockoutCache } from '../services/tenantLockoutService';
|
||||
import {
|
||||
onSessionExpired,
|
||||
triggerSessionExpired,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
cancelSessionExpiration,
|
||||
dismissSubordinateModals,
|
||||
} from '../services/sessionExpiration';
|
||||
import { resolveUserRolesAndClaims } from '../services/roleClaimsCache';
|
||||
import { resolveUserRolesAndClaims, clearCachedUserRbac } from '../services/roleClaimsCache';
|
||||
|
||||
export interface AuthUserProfile {
|
||||
id: string;
|
||||
@@ -431,9 +432,16 @@ 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);
|
||||
if (tenantClaim) {
|
||||
setTenantName(tenantClaim);
|
||||
}
|
||||
|
||||
const initialRoles = extractUserRoles(mergedClaims, null, token);
|
||||
const initialPrimaryRole =
|
||||
@@ -446,7 +454,11 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
const profile: AuthUserProfile = {
|
||||
id: userSub || 'user-1',
|
||||
sub: userSub,
|
||||
name: oidcUser.profile.name || oidcUser.profile.preferred_username || (mergedClaims.name as string) || 'Partner User',
|
||||
name:
|
||||
oidcUser.profile.name ||
|
||||
oidcUser.profile.preferred_username ||
|
||||
(mergedClaims.name as string) ||
|
||||
'Partner User',
|
||||
email: oidcUser.profile.email || (mergedClaims.email as string) || '',
|
||||
role: initialPrimaryRole,
|
||||
organisationName: (mergedClaims.organisation as string) || (mergedClaims.org as string) || undefined,
|
||||
@@ -459,27 +471,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Step 1: Sync user account FIRST (/v1/api/integration/sync) before calling any ActiveUserFilter-protected endpoints
|
||||
try {
|
||||
const syncResult = await syncUserAccount(userSub, token, profile, true);
|
||||
if (syncResult.isUnauthorized) {
|
||||
@@ -487,34 +479,73 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
window.location.replace('/');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Resolve user roles & claims from /v1/api/users/{userSub}/roles and /claims
|
||||
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 {}
|
||||
|
||||
if (syncResult.isActivationPending) {
|
||||
setIsPendingActivation(true);
|
||||
setIsSynced(false);
|
||||
setActivationError(syncResult.errorMessage || null);
|
||||
} else if (syncResult.success) {
|
||||
setIsPendingActivation(false);
|
||||
setIsSynced(true);
|
||||
setActivationError(null);
|
||||
|
||||
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 {}
|
||||
setIsLoading(false);
|
||||
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));
|
||||
|
||||
setActivationPending(false);
|
||||
setIsPendingActivation(false);
|
||||
setIsSynced(true);
|
||||
setActivationError(null);
|
||||
} catch (syncErr: any) {
|
||||
if (syncErr?.response?.status === 401) {
|
||||
await logout();
|
||||
@@ -558,10 +589,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) {
|
||||
@@ -582,13 +613,73 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
setTenantName(storedTenantClaim);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
const hasInitialPartnerRole = hasPartnerRole(extractUserRoles(parsedClaims, resolvedUser, storedToken));
|
||||
const hadPendingActivation = isActivationPending();
|
||||
|
||||
(async () => {
|
||||
// If the user already has the Partner role and is not pending activation, unblock UI immediately and verify in background
|
||||
if (hasInitialPartnerRole && !hadPendingActivation) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
const verifyPersistedSession = async () => {
|
||||
try {
|
||||
// Step 1: Sync user account FIRST (/v1/api/integration/sync)
|
||||
const syncResult = await syncUserAccount(
|
||||
storedSub || '',
|
||||
storedToken,
|
||||
resolvedUser || undefined,
|
||||
hadPendingActivation
|
||||
);
|
||||
|
||||
if (syncResult.isUnauthorized) {
|
||||
await logout();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Resolve user roles & 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 {}
|
||||
|
||||
if (syncResult.isActivationPending) {
|
||||
setIsPendingActivation(true);
|
||||
setIsSynced(false);
|
||||
setActivationError(syncResult.errorMessage || null);
|
||||
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(
|
||||
storedTenantId ?? tenantClaimValues.tenantId,
|
||||
storedTenantClaim
|
||||
effectiveTenantId,
|
||||
storedTenantClaim,
|
||||
hadPendingActivation
|
||||
);
|
||||
|
||||
if (!tenantLockoutResult.isActive) {
|
||||
@@ -601,61 +692,25 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
if (tenantLockoutResult.tenant?.name) {
|
||||
if (resolvedUser) {
|
||||
resolvedUser.tenant = tenantLockoutResult.tenant.name;
|
||||
setUser({ ...resolvedUser });
|
||||
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(resolvedUser));
|
||||
}
|
||||
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);
|
||||
setActivationError(syncResult.errorMessage || null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (syncResult.success) {
|
||||
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));
|
||||
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);
|
||||
}
|
||||
setClaims({ ...resolved.claims });
|
||||
localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims));
|
||||
}
|
||||
if (resolvedUser) {
|
||||
setUser({ ...resolvedUser });
|
||||
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(resolvedUser));
|
||||
}
|
||||
|
||||
setActivationPending(false);
|
||||
setIsPendingActivation(false);
|
||||
setIsSynced(true);
|
||||
setActivationError(null);
|
||||
} catch (syncErr: any) {
|
||||
if (syncErr?.response?.status === 401) {
|
||||
await logout();
|
||||
@@ -664,8 +719,16 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
setIsPendingActivation(true);
|
||||
setIsSynced(false);
|
||||
setActivationError(syncErr?.message || 'Account activation pending');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
if (hasInitialPartnerRole && !hadPendingActivation) {
|
||||
verifyPersistedSession();
|
||||
} else {
|
||||
await verifyPersistedSession();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -682,11 +745,68 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
|
||||
const syncUserSession = async (force = true): Promise<UserSyncResult> => {
|
||||
try {
|
||||
const tenantClaimValues = extractTenantClaimValues(claims, accessToken);
|
||||
const currentTenantId = tenantId ?? tenantClaimValues.tenantId;
|
||||
const currentTenantClaim = (claims.tenant as string) || user?.tenant || tenantClaimValues.tenant || undefined;
|
||||
await loadRuntimeConfig();
|
||||
clearTenantLockoutCache();
|
||||
if (sub) {
|
||||
clearCachedUserRbac(sub);
|
||||
}
|
||||
|
||||
const tenantLockoutResult = await verifyTenantLockout(currentTenantId, currentTenantClaim);
|
||||
// Step 1: Sync user account 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 & 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 {}
|
||||
|
||||
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);
|
||||
@@ -699,20 +819,20 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
};
|
||||
}
|
||||
|
||||
const result = await syncUserAccount(sub, accessToken, user || undefined, force);
|
||||
if (result.isUnauthorized) {
|
||||
await logout();
|
||||
return result;
|
||||
if (tenantLockoutResult.tenant?.name) {
|
||||
setTenantName(tenantLockoutResult.tenant.name);
|
||||
}
|
||||
if (result.success) {
|
||||
setIsPendingActivation(false);
|
||||
setIsSynced(true);
|
||||
setActivationError(null);
|
||||
} else if (result.isActivationPending) {
|
||||
setIsPendingActivation(true);
|
||||
setIsSynced(false);
|
||||
setActivationError(result.errorMessage || null);
|
||||
if (tenantLockoutResult.tenant?.id !== undefined && !tenantId) {
|
||||
const numericTid = Number(tenantLockoutResult.tenant.id);
|
||||
if (!isNaN(numericTid) && numericTid > 0) {
|
||||
setTenantId(numericTid);
|
||||
}
|
||||
}
|
||||
|
||||
setActivationPending(false);
|
||||
setIsPendingActivation(false);
|
||||
setIsSynced(true);
|
||||
setActivationError(null);
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 401) {
|
||||
@@ -753,6 +873,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);
|
||||
setSub('');
|
||||
setClaims({});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { updateHasherSettings } from './services/toolsService';
|
||||
import { updateHasherSettings as updateToolsHasherSettings } from './services/toolsService';
|
||||
import { updateHasherSettings as updateUtilsHasherSettings } from './utils/hashids';
|
||||
|
||||
export interface SecuritySettings {
|
||||
Authority: string;
|
||||
@@ -30,8 +31,8 @@ export interface AppConfig {
|
||||
export const defaultSecuritySettings: SecuritySettings = {
|
||||
Authority: 'https://sts.security.khongisa.co.za',
|
||||
Audience: 'pfm-api-dev',
|
||||
ClientId: 'pfm-scalar-dev',
|
||||
ClientSecret: 'secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f0276cb7431c8872d27',
|
||||
ClientId: 'pfm-console-dev',
|
||||
ClientSecret: 'secret_dcaeaa28097d9ba5b06ec324d9eac9d9c81de96e0a203cf2c6154c6dd7c89e94',
|
||||
Scope: 'openid profile roles email address pfm-api-dev',
|
||||
};
|
||||
|
||||
@@ -44,11 +45,18 @@ export const defaultHasherSettings: HasherSettings = {
|
||||
MinHashLength: 11,
|
||||
};
|
||||
|
||||
function syncAllHasherSettings(salt: string, minHashLength: number): void {
|
||||
updateToolsHasherSettings(salt, minHashLength);
|
||||
updateUtilsHasherSettings(salt, minHashLength);
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(...candidates: any[]): string | undefined {
|
||||
for (const c of candidates) {
|
||||
if (c !== undefined && c !== null) {
|
||||
const s = String(c).trim();
|
||||
if (s !== '' && s !== 'undefined' && s !== 'null') return s;
|
||||
if (s !== '' && s !== 'undefined' && s !== 'null') {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -56,13 +64,17 @@ function pickFirstNonEmpty(...candidates: any[]): string | undefined {
|
||||
|
||||
function normalizeBaseUrl(url: string): string {
|
||||
let trimmed = url.trim();
|
||||
if (!/^https?:\/\//i.test(trimmed)) trimmed = `https://${trimmed}`;
|
||||
if (!/^https?:\/\//i.test(trimmed)) {
|
||||
trimmed = `https://${trimmed}`;
|
||||
}
|
||||
return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
|
||||
}
|
||||
|
||||
function normalizeAuthorityUrl(url: string): string {
|
||||
let trimmed = url.trim();
|
||||
if (!/^https?:\/\//i.test(trimmed)) trimmed = `https://${trimmed}`;
|
||||
if (!/^https?:\/\//i.test(trimmed)) {
|
||||
trimmed = `https://${trimmed}`;
|
||||
}
|
||||
return trimmed.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
@@ -72,44 +84,178 @@ function getWindowRuntimeConfig(): Record<string, any> {
|
||||
return win.__RUNTIME_CONFIG__ || win.APP_CONFIG || win._env_ || {};
|
||||
}
|
||||
|
||||
function extractSourceConfig(src: Record<string, any>) {
|
||||
function extractSourceConfig(src: Record<string, any>): {
|
||||
authority?: string;
|
||||
audience?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
scope?: string;
|
||||
baseUrl?: string;
|
||||
hasherSalt?: string;
|
||||
hasherMinLength?: number;
|
||||
} {
|
||||
if (!src || typeof src !== 'object') return {};
|
||||
|
||||
const sec = src.SecuritySettings || src.securitySettings || {};
|
||||
const api = src.ApiSettings || src.apiSettings || {};
|
||||
const hasher = src.HasherSettings || src.hasherSettings || {};
|
||||
|
||||
const minLenRaw = pickFirstNonEmpty(
|
||||
src.VITE_HASHER_MIN_LENGTH,
|
||||
src.HASHER_MIN_LENGTH,
|
||||
src.HasherSettings__MinHashLength,
|
||||
hasher.MinHashLength,
|
||||
hasher.minHashLength,
|
||||
src.MinHashLength,
|
||||
src.minHashLength
|
||||
);
|
||||
const parsedMinLen = minLenRaw !== undefined ? Number(minLenRaw) : undefined;
|
||||
|
||||
return {
|
||||
authority: pickFirstNonEmpty(src.VITE_OIDC_AUTHORITY, src.OIDC_AUTHORITY, src.SecuritySettings?.Authority, src.SecuritySettings__Authority),
|
||||
audience: pickFirstNonEmpty(src.VITE_OIDC_AUDIENCE, src.OIDC_AUDIENCE, src.SecuritySettings?.Audience, src.SecuritySettings__Audience),
|
||||
clientId: pickFirstNonEmpty(src.VITE_OIDC_CLIENT_ID, src.OIDC_CLIENT_ID, src.SecuritySettings?.ClientId, src.SecuritySettings__ClientId),
|
||||
clientSecret: pickFirstNonEmpty(src.VITE_OIDC_CLIENT_SECRET, src.OIDC_CLIENT_SECRET, src.SecuritySettings?.ClientSecret, src.SecuritySettings__ClientSecret),
|
||||
scope: pickFirstNonEmpty(src.VITE_OIDC_SCOPE, src.OIDC_SCOPE, src.SecuritySettings?.Scope, src.SecuritySettings__Scope),
|
||||
baseUrl: pickFirstNonEmpty(src.VITE_API_BASE_URL, src.API_BASE_URL, src.ApiSettings?.BaseUrl, src.ApiSettings__BaseUrl),
|
||||
hasherSalt: pickFirstNonEmpty(src.VITE_HASHER_SALT, src.HASHER_SALT, src.HasherSettings?.Salt, src.HasherSettings__Salt),
|
||||
hasherMinLength: Number(pickFirstNonEmpty(src.VITE_HASHER_MIN_LENGTH, src.HASHER_MIN_LENGTH, src.HasherSettings?.MinHashLength)) || undefined,
|
||||
authority: pickFirstNonEmpty(
|
||||
src.VITE_OIDC_AUTHORITY,
|
||||
src.OIDC_AUTHORITY,
|
||||
src.SecuritySettings__Authority,
|
||||
sec.Authority,
|
||||
sec.authority,
|
||||
src.Authority,
|
||||
src.authority
|
||||
),
|
||||
audience: pickFirstNonEmpty(
|
||||
src.VITE_OIDC_AUDIENCE,
|
||||
src.OIDC_AUDIENCE,
|
||||
src.SecuritySettings__Audience,
|
||||
sec.Audience,
|
||||
sec.audience,
|
||||
src.Audience,
|
||||
src.audience
|
||||
),
|
||||
clientId: pickFirstNonEmpty(
|
||||
src.VITE_OIDC_CLIENT_ID,
|
||||
src.OIDC_CLIENT_ID,
|
||||
src.SecuritySettings__ClientId,
|
||||
sec.ClientId,
|
||||
sec.clientId,
|
||||
src.ClientId,
|
||||
src.clientId
|
||||
),
|
||||
clientSecret: pickFirstNonEmpty(
|
||||
src.VITE_OIDC_CLIENT_SECRET,
|
||||
src.OIDC_CLIENT_SECRET,
|
||||
src.SecuritySettings__ClientSecret,
|
||||
sec.ClientSecret,
|
||||
sec.clientSecret,
|
||||
src.ClientSecret,
|
||||
src.clientSecret
|
||||
),
|
||||
scope: pickFirstNonEmpty(
|
||||
src.VITE_OIDC_SCOPE,
|
||||
src.OIDC_SCOPE,
|
||||
src.SecuritySettings__Scope,
|
||||
sec.Scope,
|
||||
sec.scope,
|
||||
src.Scope,
|
||||
src.scope
|
||||
),
|
||||
baseUrl: pickFirstNonEmpty(
|
||||
src.VITE_API_BASE_URL,
|
||||
src.API_BASE_URL,
|
||||
src.ApiSettings__BaseUrl,
|
||||
api.BaseUrl,
|
||||
api.baseUrl,
|
||||
src.BaseUrl,
|
||||
src.baseUrl
|
||||
),
|
||||
hasherSalt: pickFirstNonEmpty(
|
||||
src.VITE_HASHER_SALT,
|
||||
src.HASHER_SALT,
|
||||
src.HasherSettings__Salt,
|
||||
hasher.Salt,
|
||||
hasher.salt,
|
||||
src.Salt,
|
||||
src.salt
|
||||
),
|
||||
hasherMinLength:
|
||||
parsedMinLen !== undefined && !isNaN(parsedMinLen) && parsedMinLen > 0
|
||||
? parsedMinLen
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveConfig(fetchedJson?: Record<string, any>): AppConfig {
|
||||
const winCfg = extractSourceConfig(getWindowRuntimeConfig());
|
||||
const fetchCfg = fetchedJson ? extractSourceConfig(fetchedJson) : {};
|
||||
const envCfg = extractSourceConfig((typeof import.meta !== 'undefined' && (import.meta as any).env ? (import.meta as any).env : {}) as Record<string, any>);
|
||||
const envCfg = extractSourceConfig(
|
||||
(typeof import.meta !== 'undefined' && (import.meta as any).env
|
||||
? (import.meta as any).env
|
||||
: {}) as Record<string, any>
|
||||
);
|
||||
|
||||
const hasWin = Object.values(winCfg).some((v) => v !== undefined);
|
||||
const hasFetch = Object.values(fetchCfg).some((v) => v !== undefined);
|
||||
const hasEnv = Object.values(envCfg).some((v) => v !== undefined);
|
||||
|
||||
let source: ConfigSource = 'fallback-defaults';
|
||||
if (Object.values(winCfg).some((v) => v !== undefined)) source = 'runtime-injected';
|
||||
else if (Object.values(fetchCfg).some((v) => v !== undefined)) source = 'runtime-fetched';
|
||||
else if (Object.values(envCfg).some((v) => v !== undefined)) source = 'build-env';
|
||||
if (hasWin) source = 'runtime-injected';
|
||||
else if (hasFetch) source = 'runtime-fetched';
|
||||
else if (hasEnv) source = 'build-env';
|
||||
|
||||
const authority = normalizeAuthorityUrl(
|
||||
pickFirstNonEmpty(winCfg.authority, fetchCfg.authority, envCfg.authority, defaultSecuritySettings.Authority)!
|
||||
);
|
||||
const audience = pickFirstNonEmpty(
|
||||
winCfg.audience,
|
||||
fetchCfg.audience,
|
||||
envCfg.audience,
|
||||
defaultSecuritySettings.Audience
|
||||
)!;
|
||||
const clientId = pickFirstNonEmpty(
|
||||
winCfg.clientId,
|
||||
fetchCfg.clientId,
|
||||
envCfg.clientId,
|
||||
defaultSecuritySettings.ClientId
|
||||
)!;
|
||||
const clientSecret = pickFirstNonEmpty(
|
||||
winCfg.clientSecret,
|
||||
fetchCfg.clientSecret,
|
||||
envCfg.clientSecret,
|
||||
defaultSecuritySettings.ClientSecret
|
||||
)!;
|
||||
const scope = pickFirstNonEmpty(
|
||||
winCfg.scope,
|
||||
fetchCfg.scope,
|
||||
envCfg.scope,
|
||||
defaultSecuritySettings.Scope
|
||||
);
|
||||
const baseUrl = normalizeBaseUrl(
|
||||
pickFirstNonEmpty(winCfg.baseUrl, fetchCfg.baseUrl, envCfg.baseUrl, defaultApiSettings.BaseUrl)!
|
||||
);
|
||||
const hasherSalt = pickFirstNonEmpty(
|
||||
winCfg.hasherSalt,
|
||||
fetchCfg.hasherSalt,
|
||||
envCfg.hasherSalt,
|
||||
defaultHasherSettings.Salt
|
||||
)!;
|
||||
const hasherMinLength =
|
||||
winCfg.hasherMinLength ||
|
||||
fetchCfg.hasherMinLength ||
|
||||
envCfg.hasherMinLength ||
|
||||
defaultHasherSettings.MinHashLength;
|
||||
|
||||
return {
|
||||
SecuritySettings: {
|
||||
Authority: normalizeAuthorityUrl(pickFirstNonEmpty(winCfg.authority, fetchCfg.authority, envCfg.authority, defaultSecuritySettings.Authority)!),
|
||||
Audience: pickFirstNonEmpty(winCfg.audience, fetchCfg.audience, envCfg.audience, defaultSecuritySettings.Audience)!,
|
||||
ClientId: pickFirstNonEmpty(winCfg.clientId, fetchCfg.clientId, envCfg.clientId, defaultSecuritySettings.ClientId)!,
|
||||
ClientSecret: pickFirstNonEmpty(winCfg.clientSecret, fetchCfg.clientSecret, envCfg.clientSecret, defaultSecuritySettings.ClientSecret)!,
|
||||
Scope: pickFirstNonEmpty(winCfg.scope, fetchCfg.scope, envCfg.scope, defaultSecuritySettings.Scope),
|
||||
Authority: authority,
|
||||
Audience: audience,
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
Scope: scope,
|
||||
},
|
||||
ApiSettings: {
|
||||
BaseUrl: normalizeBaseUrl(pickFirstNonEmpty(winCfg.baseUrl, fetchCfg.baseUrl, envCfg.baseUrl, defaultApiSettings.BaseUrl)!),
|
||||
BaseUrl: baseUrl,
|
||||
},
|
||||
HasherSettings: {
|
||||
Salt: pickFirstNonEmpty(winCfg.hasherSalt, fetchCfg.hasherSalt, envCfg.hasherSalt, defaultHasherSettings.Salt)!,
|
||||
MinHashLength: winCfg.hasherMinLength || fetchCfg.hasherMinLength || envCfg.hasherMinLength || defaultHasherSettings.MinHashLength,
|
||||
Salt: hasherSalt,
|
||||
MinHashLength: hasherMinLength,
|
||||
},
|
||||
ConfigSource: source,
|
||||
LoadedAt: new Date().toISOString(),
|
||||
@@ -117,7 +263,7 @@ function resolveConfig(fetchedJson?: Record<string, any>): AppConfig {
|
||||
}
|
||||
|
||||
let runtimeConfig: AppConfig = resolveConfig();
|
||||
updateHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
||||
syncAllHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
||||
|
||||
const configListeners: Array<(config: AppConfig) => void> = [];
|
||||
|
||||
@@ -129,28 +275,49 @@ export function onConfigChanged(listener: (config: AppConfig) => void): () => vo
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadRuntimeConfig(): Promise<AppConfig> {
|
||||
let fetchedJson: Record<string, any> | undefined;
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const res = await fetch(`/config.json?_t=${Date.now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: { Accept: 'application/json' },
|
||||
}).catch(() => null);
|
||||
if (res && res.ok) {
|
||||
fetchedJson = await res.json().catch(() => undefined);
|
||||
}
|
||||
} catch {
|
||||
// Safe fallback
|
||||
}
|
||||
let loadPromise: Promise<AppConfig> | null = null;
|
||||
|
||||
export async function loadRuntimeConfig(forceReload = false): Promise<AppConfig> {
|
||||
if (loadPromise && !forceReload) {
|
||||
return loadPromise;
|
||||
}
|
||||
runtimeConfig = resolveConfig(fetchedJson);
|
||||
updateHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
||||
configListeners.forEach((fn) => fn(runtimeConfig));
|
||||
return runtimeConfig;
|
||||
|
||||
loadPromise = (async () => {
|
||||
let fetchedJson: Record<string, any> | undefined;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const res = await fetch(`/config.json?_t=${Date.now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: { Accept: 'application/json' },
|
||||
}).catch(() => null);
|
||||
|
||||
if (res && res.ok) {
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json') || contentType.includes('text/plain')) {
|
||||
fetchedJson = await res.json().catch(() => undefined);
|
||||
} else {
|
||||
const text = await res.text().catch(() => '');
|
||||
if (text.trim().startsWith('{')) {
|
||||
fetchedJson = JSON.parse(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fallback if /config.json is unreachable
|
||||
}
|
||||
}
|
||||
|
||||
runtimeConfig = resolveConfig(fetchedJson);
|
||||
syncAllHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
||||
configListeners.forEach((fn) => fn(runtimeConfig));
|
||||
return runtimeConfig;
|
||||
})();
|
||||
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
export const getAppConfig = () => runtimeConfig;
|
||||
export const getSecuritySettings = () => runtimeConfig.SecuritySettings;
|
||||
export const getApiBaseUrl = () => runtimeConfig.ApiSettings.BaseUrl;
|
||||
export const getHasherSettings = () => runtimeConfig.HasherSettings;
|
||||
export const getAppConfig = (): AppConfig => runtimeConfig;
|
||||
export const getSecuritySettings = (): SecuritySettings => runtimeConfig.SecuritySettings;
|
||||
export const getApiBaseUrl = (): string => runtimeConfig.ApiSettings.BaseUrl;
|
||||
export const getHasherSettings = (): HasherSettings => runtimeConfig.HasherSettings;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
import { loadRuntimeConfig } from './config';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
// Load external runtime configuration (/config.json or window.__RUNTIME_CONFIG__)
|
||||
// before mounting the React application so OIDC, API BaseUrl, and HashIds use runtime values.
|
||||
loadRuntimeConfig().finally(() => {
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -343,12 +343,15 @@ export async function resolveUserRolesAndClaims(
|
||||
}
|
||||
});
|
||||
if (resolvedRoles.length > 0) {
|
||||
primaryRole = resolvedRoles[0];
|
||||
const partnerMatch = resolvedRoles.find((r) => r.toLowerCase().trim() === 'partner');
|
||||
primaryRole = partnerMatch || resolvedRoles[0];
|
||||
mergedClaims.roles = resolvedRoles;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedRoles.length === 0 && primaryRole) {
|
||||
resolvedRoles.push(primaryRole);
|
||||
mergedClaims.roles = resolvedRoles;
|
||||
}
|
||||
|
||||
const allRoleClaims: Claim[] = [];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTenant } from './api';
|
||||
import { getTenant, getTenants } from './api';
|
||||
import { ActivityStatus, Tenant } from '../types';
|
||||
import { setActivationPending } from './userSyncService';
|
||||
import { encodeHashIdSync, isAlreadyHashId } from './toolsService';
|
||||
|
||||
export interface TenantLockoutResult {
|
||||
isActive: boolean;
|
||||
@@ -58,30 +59,36 @@ export function getStatusLabel(status: any): string {
|
||||
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<TenantLockoutResult> {
|
||||
const primaryId =
|
||||
tenantId !== undefined && tenantId !== null && tenantId !== ''
|
||||
? tenantId
|
||||
: tenantClaim !== undefined && tenantClaim !== null && tenantClaim !== ''
|
||||
? tenantClaim
|
||||
: null;
|
||||
|
||||
if (primaryId === null || primaryId === undefined || primaryId === '') {
|
||||
const errorMsg =
|
||||
'Tenant identification claims (tenant / tenant_id) not found in credentials. Access is restricted.';
|
||||
setActivationPending(true, errorMsg);
|
||||
return {
|
||||
isActive: false,
|
||||
tenant: null,
|
||||
errorMessage: errorMsg,
|
||||
};
|
||||
// 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(primaryId).trim().toLowerCase();
|
||||
const cacheKey = String(candidateId ?? tenantClaim ?? 'default-1').trim().toLowerCase();
|
||||
if (!forceRefresh && cachedTenantLockout && cachedTenantLockout.key === cacheKey) {
|
||||
if (
|
||||
cachedTenantLockout.result.isActive &&
|
||||
@@ -94,23 +101,61 @@ export async function verifyTenantLockout(
|
||||
let tenantRes: Tenant | null = null;
|
||||
let lastError: any = null;
|
||||
|
||||
try {
|
||||
tenantRes = await getTenant(primaryId);
|
||||
} catch (err: any) {
|
||||
lastError = err;
|
||||
tenantRes = null;
|
||||
}
|
||||
|
||||
if (!tenantRes && tenantClaim && String(tenantClaim) !== String(primaryId)) {
|
||||
// 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 {
|
||||
tenantRes = await getTenant(tenantClaim);
|
||||
lastError = null;
|
||||
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)) {
|
||||
const serverMsg =
|
||||
lastError?.response?.data?.detail ||
|
||||
@@ -140,7 +185,7 @@ export async function verifyTenantLockout(
|
||||
|
||||
if (!active) {
|
||||
const statusLabel = getStatusLabel(rawStatus);
|
||||
const tenantName = tenantObj?.name || String(primaryId);
|
||||
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 {
|
||||
@@ -150,6 +195,8 @@ export async function verifyTenantLockout(
|
||||
};
|
||||
}
|
||||
|
||||
setActivationPending(false);
|
||||
|
||||
const successResult: TenantLockoutResult = {
|
||||
isActive: true,
|
||||
tenant: tenantObj,
|
||||
@@ -161,4 +208,3 @@ export async function verifyTenantLockout(
|
||||
};
|
||||
return successResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,13 +36,20 @@ export function updateHasherSettings(salt?: string, minLength?: number | string)
|
||||
}
|
||||
}
|
||||
|
||||
function isAlreadyHashId(candidate: string): boolean {
|
||||
if (typeof candidate !== 'string' || candidate.length < HASHER_SETTINGS.minHashLength) {
|
||||
export function isAlreadyHashId(candidate: string): boolean {
|
||||
if (
|
||||
typeof candidate !== 'string' ||
|
||||
candidate.length < HASHER_SETTINGS.minHashLength ||
|
||||
/^\d+$/.test(candidate)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const decoded = hasher.decode(candidate);
|
||||
return Array.isArray(decoded) && decoded.length > 0;
|
||||
if (!Array.isArray(decoded) || decoded.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return hasher.encode(decoded as number[]) === candidate;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -35,14 +35,31 @@ export function updateHasherSettings(salt?: string, minLength?: number | string)
|
||||
* Validates if candidate is already a HashID generated by this hasher.
|
||||
*/
|
||||
export function isHashId(candidate: string | null | undefined): boolean {
|
||||
if (!candidate || typeof candidate !== 'string' || candidate.length < HASHER_SETTINGS.minHashLength) {
|
||||
if (
|
||||
!candidate ||
|
||||
typeof candidate !== 'string' ||
|
||||
candidate.length < HASHER_SETTINGS.minHashLength ||
|
||||
/^\d+$/.test(candidate)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const decodedNumbers = hasher.decode(candidate);
|
||||
if (Array.isArray(decodedNumbers) && decodedNumbers.length > 0) return true;
|
||||
if (
|
||||
Array.isArray(decodedNumbers) &&
|
||||
decodedNumbers.length > 0 &&
|
||||
hasher.encode(decodedNumbers as number[]) === candidate
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const decodedHex = hasher.decodeHex(candidate);
|
||||
if (decodedHex && decodedHex.length > 0) return true;
|
||||
if (
|
||||
decodedHex &&
|
||||
decodedHex.length > 0 &&
|
||||
hasher.encodeHex(decodedHex) === candidate
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user