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({});
|
||||
|
||||
Reference in New Issue
Block a user