Added tenant management

Applied basic access control
This commit is contained in:
2026-10-04 09:13:55 +02:00
parent 8100518549
commit 8fed46961a
7 changed files with 1349 additions and 22 deletions
@@ -13,6 +13,7 @@ import {
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';
@@ -44,7 +45,11 @@ interface AuthContextType {
claims: UserClaims;
tenantId: number | null;
hashedTenantId: string | null;
tenantName: string | null;
accessToken: string | null;
roles: string[];
isPartner: boolean;
canManageTenants: boolean;
isAuthenticated: boolean;
isLoading: boolean;
isSynced: boolean;
@@ -65,6 +70,73 @@ export const LOCAL_STORAGE_USER_KEY = 'pfm_auth_user';
export const LOCAL_STORAGE_SUB_KEY = 'pfm_user_sub';
export const LOCAL_STORAGE_CLAIMS_KEY = 'pfm_user_claims';
export function extractUserRoles(
claims?: UserClaims | null,
user?: AuthUserProfile | null,
token?: string | null
): string[] {
const rolesSet = new Set<string>();
const processRoles = (raw: any) => {
if (!raw) return;
if (Array.isArray(raw)) {
raw.forEach((r) => {
if (typeof r === 'string' && r.trim()) rolesSet.add(r.trim());
});
} else if (typeof raw === 'string' && raw.trim()) {
if (raw.includes(',')) {
raw.split(',').forEach((r) => {
if (r.trim()) rolesSet.add(r.trim());
});
} else {
rolesSet.add(raw.trim());
}
}
};
if (claims) {
processRoles(claims.roles);
processRoles(claims.role);
processRoles(claims['roles']);
processRoles(claims['role']);
processRoles(claims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role']);
processRoles(claims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role']);
processRoles(claims['realm_access']?.roles);
processRoles(claims['resource_access']?.roles);
}
if (token) {
try {
const tokenClaims = parseJwtClaims(token);
processRoles(tokenClaims.roles);
processRoles(tokenClaims.role);
processRoles(tokenClaims['roles']);
processRoles(tokenClaims['role']);
processRoles(tokenClaims['http://schemas.microsoft.com/ws/2008/06/identity/claims/role']);
processRoles(tokenClaims['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role']);
} catch {}
}
if (user?.role && typeof user.role === 'string') {
processRoles(user.role);
}
if ((user as any)?.roles) {
processRoles((user as any).roles);
}
return Array.from(rolesSet);
}
export function hasPartnerRole(roles: string[]): boolean {
return roles.some((r) => r.toLowerCase() === 'partner');
}
export function canManageTenants(tenantId?: number | string | null, roles?: string[]): boolean {
const isTenantOne = Number(tenantId) === 1;
const hasAdmin = Array.isArray(roles) && roles.some((r) => r.toLowerCase() === 'admin');
return isTenantOne && hasAdmin;
}
export function extractTenantId(claims?: UserClaims | null, token?: string | null): number | null {
if (claims) {
const raw =
@@ -272,6 +344,12 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY) : null
);
});
const [tenantName, setTenantName] = useState<string | null>(() => {
const claims = getCurrentUserClaims();
const token = typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY) : null;
const { tenant } = extractTenantClaimValues(claims, token);
return tenant;
});
const [accessToken, setAccessToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isSynced, setIsSynced] = useState<boolean>(() => isUserSyncedInBrowser());
@@ -393,6 +471,14 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
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
try {
const syncResult = await syncUserAccount(userSub, token, profile, true);
@@ -488,6 +574,17 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
return;
}
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);
} else if (storedTenantClaim) {
setTenantName(storedTenantClaim);
}
// Check if session is already marked pending activation or needs sync check
const wasPending = isActivationPending();
setIsPendingActivation(wasPending);
@@ -633,6 +730,18 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}
};
const roles = React.useMemo(() => {
return extractUserRoles(claims, user, accessToken);
}, [claims, user, accessToken]);
const isPartner = React.useMemo(() => {
return hasPartnerRole(roles);
}, [roles]);
const canManageTenantsAccess = React.useMemo(() => {
return canManageTenants(tenantId, roles);
}, [tenantId, roles]);
return (
<AuthContext.Provider
value={{
@@ -641,7 +750,11 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
claims,
tenantId,
hashedTenantId,
tenantName,
accessToken,
roles,
isPartner,
canManageTenants: canManageTenantsAccess,
isAuthenticated: !!accessToken,
isLoading,
isSynced,
@@ -680,6 +793,8 @@ export const ProtectedRoute: React.FC<{ children: ReactNode }> = ({ children })
isAuthenticated,
isLoading,
isPendingActivation,
isPartner,
roles,
user,
claims,
sub,
@@ -707,6 +822,18 @@ export const ProtectedRoute: React.FC<{ children: ReactNode }> = ({ children })
return <Navigate to="/" state={{ from: location }} replace />;
}
// Security Access Control: Users assigned the Partner role are denied entry to the console
if (isPartner || hasPartnerRole(roles)) {
return (
<PartnerAccessDeniedModal
user={user}
claims={claims}
sub={sub}
onLogout={logout}
/>
);
}
// If user is authenticated but account has not yet been enabled by an administrator:
// Render the non-dismissable activation modal, blocking all downstream layout and queries.
if (isPendingActivation) {
@@ -724,3 +851,18 @@ export const ProtectedRoute: React.FC<{ children: ReactNode }> = ({ children })
return <>{children}</>;
};
// Guard for Tenant Management (tenant_id == 1 and role list has Admin)
export const TenantAdminProtectedRoute: React.FC<{ children: ReactNode }> = ({ children }) => {
const { canManageTenants: allowed, isLoading } = useAuth();
if (isLoading) {
return null;
}
if (!allowed) {
return <Navigate to="/dashboard" replace />;
}
return <>{children}</>;
};