Added tenant management
Applied basic access control
This commit is contained in:
@@ -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}</>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user