Fixed tenant management
This commit is contained in:
@@ -138,7 +138,7 @@ export const AppLayout: React.FC = () => {
|
||||
const visibleNavItems = navItems
|
||||
.filter((item) => {
|
||||
if (item.path === '/tenants') {
|
||||
return canManageTenants || canView('tenant_management');
|
||||
return canManageTenants || canView('tenant_management') || canView('tenant_self_service');
|
||||
}
|
||||
if (item.path === '/settings') {
|
||||
return (
|
||||
@@ -456,7 +456,7 @@ export const AppLayout: React.FC = () => {
|
||||
|
||||
<div className="hidden sm:flex items-center gap-2 text-xs font-medium text-slate-300">
|
||||
<span className="text-slate-400">Tenant:</span>
|
||||
{canManageTenants ? (
|
||||
{canManageTenants || canView('tenant_management') || canView('tenant_self_service') ? (
|
||||
<Link
|
||||
to="/tenants"
|
||||
className="bg-slate-900/80 hover:bg-slate-800/90 text-slate-200 hover:text-white px-2.5 py-1 rounded-lg border border-slate-800 hover:border-slate-700 flex items-center gap-1.5 font-medium shadow-xs max-w-[320px] truncate transition group cursor-pointer"
|
||||
|
||||
@@ -54,9 +54,23 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
const { canApprove, canAdd, canView, canAccess, isUnrestricted } = useRbac();
|
||||
const hasTenantSelfHelpAccess = canAccess('tenant_self_service') || canView('tenant_self_service');
|
||||
|
||||
const canManageRolesAndClaims = canApprove('settings_users') || canApprove('tenant_management') || isUnrestricted;
|
||||
const canManageSessions = canApprove('settings_users') || canApprove('tenant_management') || hasTenantSelfHelpAccess || isUnrestricted;
|
||||
const canChangeStatus = canAdd('settings_users') || canAdd('tenant_management') || canApprove('settings_users') || isUnrestricted;
|
||||
const canManageRolesAndClaims =
|
||||
canApprove('settings_users') ||
|
||||
canApprove('tenant_management') ||
|
||||
canApprove('tenant_self_service') ||
|
||||
canAdd('tenant_self_service') ||
|
||||
isUnrestricted;
|
||||
const canManageSessions =
|
||||
canApprove('settings_users') ||
|
||||
canApprove('tenant_management') ||
|
||||
hasTenantSelfHelpAccess ||
|
||||
isUnrestricted;
|
||||
const canChangeStatus =
|
||||
canAdd('settings_users') ||
|
||||
canAdd('tenant_management') ||
|
||||
canAdd('tenant_self_service') ||
|
||||
canApprove('settings_users') ||
|
||||
isUnrestricted;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'roles' | 'claims' | 'sessions'>('overview');
|
||||
|
||||
@@ -105,8 +119,11 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
// Role Mutations
|
||||
const assignRoleMutation = useMutation({
|
||||
mutationFn: (roleId: string) => {
|
||||
const selectedRole = systemRoles.find((r) => r.id === roleId);
|
||||
if (selectedRole && (selectedRole.name.toLowerCase() === 'admin' || selectedRole.id === '1') && hasTenantSelfHelpAccess) {
|
||||
const selectedRole = (Array.isArray(systemRoles) ? systemRoles : []).find(
|
||||
(r) => r && (String(r.id) === String(roleId) || String((r as any).Id) === String(roleId))
|
||||
);
|
||||
const roleName = selectedRole?.name || (selectedRole as any)?.Name || '';
|
||||
if (selectedRole && (roleName.toLowerCase() === 'admin' || String(selectedRole.id) === '1') && hasTenantSelfHelpAccess) {
|
||||
throw new Error('Security policy: Users with tenant self-help access cannot assign Admin roles.');
|
||||
}
|
||||
return assignUserRole(userId, roleId);
|
||||
@@ -192,11 +209,16 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
if (!user) return null;
|
||||
|
||||
const effectiveUser = freshUser || user;
|
||||
const decodedId = effectiveUser.idNumber ? decodeIdNumberHex(effectiveUser.idNumber) : null;
|
||||
const assignedRoleIds = new Set(userRoles.map((r) => r.id));
|
||||
const availableRoles = systemRoles.filter((r) => {
|
||||
if (assignedRoleIds.has(r.id)) return false;
|
||||
if (hasTenantSelfHelpAccess && (r.name.toLowerCase() === 'admin' || r.id === '1')) {
|
||||
const decodedId = effectiveUser?.idNumber ? decodeIdNumberHex(effectiveUser.idNumber) : null;
|
||||
const assignedRoleIds = new Set(
|
||||
(Array.isArray(userRoles) ? userRoles : []).map((r) => r?.id || (r as any)?.Id).filter(Boolean)
|
||||
);
|
||||
const availableRoles = (Array.isArray(systemRoles) ? systemRoles : []).filter((r) => {
|
||||
if (!r) return false;
|
||||
const rId = r.id || (r as any).Id;
|
||||
const rName = r.name || (r as any).Name || '';
|
||||
if (rId && assignedRoleIds.has(rId)) return false;
|
||||
if (hasTenantSelfHelpAccess && (rName.toLowerCase() === 'admin' || String(rId) === '1')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -204,21 +226,28 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
|
||||
const assignableClaims = React.useMemo(() => {
|
||||
const claimMap = new Map<string, { claim: string; label: string }>();
|
||||
systemClaims.forEach((c) => {
|
||||
const type = c.claimType || c.type;
|
||||
if (type) {
|
||||
if (hasTenantSelfHelpAccess && (type.toLowerCase() === 'unrestricted' || type.toLowerCase() === 'admin')) {
|
||||
if (Array.isArray(systemClaims)) {
|
||||
systemClaims.forEach((c) => {
|
||||
if (!c) return;
|
||||
const type = c.claimType || c.type;
|
||||
if (type && typeof type === 'string') {
|
||||
if (hasTenantSelfHelpAccess && (type.toLowerCase() === 'unrestricted' || type.toLowerCase() === 'admin')) {
|
||||
return;
|
||||
}
|
||||
claimMap.set(type, { claim: type, label: type });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (Array.isArray(ROLE_CLAIM_DEFINITIONS)) {
|
||||
ROLE_CLAIM_DEFINITIONS.forEach((def) => {
|
||||
if (!def || !def.claim) return;
|
||||
const claimStr = String(def.claim);
|
||||
if (hasTenantSelfHelpAccess && (claimStr.toLowerCase() === 'unrestricted' || claimStr.toLowerCase() === 'admin')) {
|
||||
return;
|
||||
}
|
||||
claimMap.set(type, { claim: type, label: type });
|
||||
}
|
||||
});
|
||||
ROLE_CLAIM_DEFINITIONS.forEach((def) => {
|
||||
if (hasTenantSelfHelpAccess && (def.claim === 'unrestricted' || (def.claim as string) === 'admin')) {
|
||||
return;
|
||||
}
|
||||
claimMap.set(def.claim, { claim: def.claim, label: `${def.claim} (${def.label})` });
|
||||
});
|
||||
claimMap.set(claimStr, { claim: claimStr, label: `${claimStr} (${def.label || claimStr})` });
|
||||
});
|
||||
}
|
||||
return Array.from(claimMap.values());
|
||||
}, [systemClaims, hasTenantSelfHelpAccess]);
|
||||
|
||||
@@ -325,10 +354,10 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
</span>
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border ${
|
||||
getActivityStatusBadge(effectiveUser.status).className
|
||||
(getActivityStatusBadge(effectiveUser.status) || {}).className || 'bg-slate-800 text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{getActivityStatusBadge(effectiveUser.status).label}
|
||||
{(getActivityStatusBadge(effectiveUser.status) || {}).label || 'Active'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -336,8 +365,8 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => statusMutation.mutate(ActivityStatus.Inactive)}
|
||||
disabled={statusMutation.isPending}
|
||||
className="px-2.5 py-1 text-xs font-semibold text-amber-400 bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/20 rounded-lg transition"
|
||||
disabled={statusMutation.isPending || !canChangeStatus}
|
||||
className="px-2.5 py-1 text-xs font-semibold text-amber-400 bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/20 rounded-lg transition disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
Suspend User
|
||||
</button>
|
||||
@@ -345,8 +374,8 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => statusMutation.mutate(ActivityStatus.Active)}
|
||||
disabled={statusMutation.isPending}
|
||||
className="px-2.5 py-1 text-xs font-semibold text-emerald-400 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 rounded-lg transition"
|
||||
disabled={statusMutation.isPending || !canChangeStatus}
|
||||
className="px-2.5 py-1 text-xs font-semibold text-emerald-400 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 rounded-lg transition disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
Activate User
|
||||
</button>
|
||||
@@ -365,8 +394,8 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
<select
|
||||
value={selectedRoleToAdd}
|
||||
onChange={(e) => setSelectedRoleToAdd(e.target.value)}
|
||||
disabled={loadingSystemRoles || availableRoles.length === 0}
|
||||
className="flex-1 bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-white focus:outline-none focus:border-blue-500"
|
||||
disabled={loadingSystemRoles || availableRoles.length === 0 || !canManageRolesAndClaims}
|
||||
className="flex-1 bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-white focus:outline-none focus:border-blue-500 disabled:opacity-50"
|
||||
>
|
||||
<option value="">
|
||||
{availableRoles.length === 0 ? 'All system roles assigned' : 'Select role to assign...'}
|
||||
@@ -380,8 +409,8 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectedRoleToAdd && assignRoleMutation.mutate(selectedRoleToAdd)}
|
||||
disabled={!selectedRoleToAdd || assignRoleMutation.isPending}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-semibold transition disabled:opacity-50"
|
||||
disabled={!selectedRoleToAdd || assignRoleMutation.isPending || !canManageRolesAndClaims}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-semibold transition disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Assign Role</span>
|
||||
@@ -392,29 +421,34 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
<div className="bg-slate-900/60 rounded-xl border border-slate-800 divide-y divide-slate-800/80">
|
||||
{loadingUserRoles ? (
|
||||
<div className="p-4 text-center text-slate-500">Loading roles...</div>
|
||||
) : userRoles.length === 0 ? (
|
||||
) : (Array.isArray(userRoles) ? userRoles : []).length === 0 ? (
|
||||
<div className="p-4 text-center text-slate-500">No roles assigned to this user.</div>
|
||||
) : (
|
||||
userRoles.map((role) => (
|
||||
<div key={role.id} className="p-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-white font-semibold flex items-center gap-1.5">
|
||||
<Shield className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span>{role.name}</span>
|
||||
(Array.isArray(userRoles) ? userRoles : []).map((role) => {
|
||||
if (!role) return null;
|
||||
const roleId = role.id || (role as any).Id || String(role);
|
||||
const roleName = role.name || (role as any).Name || String(role);
|
||||
return (
|
||||
<div key={roleId} className="p-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-white font-semibold flex items-center gap-1.5">
|
||||
<Shield className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span>{roleName}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 font-mono">Role ID: {roleId}</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 font-mono">Role ID: {role.id}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRoleMutation.mutate(roleId)}
|
||||
disabled={removeRoleMutation.isPending || !canManageRolesAndClaims}
|
||||
className="p-1.5 text-rose-400 hover:text-rose-300 hover:bg-rose-500/10 rounded-lg transition disabled:opacity-50 cursor-pointer"
|
||||
title="Revoke Role"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRoleMutation.mutate(role.id)}
|
||||
disabled={removeRoleMutation.isPending}
|
||||
className="p-1.5 text-rose-400 hover:text-rose-300 hover:bg-rose-500/10 rounded-lg transition"
|
||||
title="Revoke Role"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -497,7 +531,7 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addClaimMutation.mutate()}
|
||||
disabled={!newClaimType.trim() || !newClaimValue.trim() || addClaimMutation.isPending}
|
||||
disabled={!newClaimType.trim() || !newClaimValue.trim() || addClaimMutation.isPending || !canManageRolesAndClaims}
|
||||
className="inline-flex items-center gap-1 px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-semibold transition disabled:opacity-50 cursor-pointer text-xs"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
@@ -510,34 +544,40 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
|
||||
<div className="bg-slate-900/60 rounded-xl border border-slate-800 divide-y divide-slate-800/80">
|
||||
{loadingUserClaims ? (
|
||||
<div className="p-4 text-center text-slate-500">Loading user claims...</div>
|
||||
) : userClaims.length === 0 ? (
|
||||
) : (Array.isArray(userClaims) ? userClaims : []).length === 0 ? (
|
||||
<div className="p-4 text-center text-slate-500">No active claims found for user.</div>
|
||||
) : (
|
||||
userClaims.map((claim, idx) => (
|
||||
<div key={idx} className="p-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-white font-medium flex items-center gap-1.5">
|
||||
<Key className="w-3.5 h-3.5 text-amber-400" />
|
||||
<span>{claim.type || 'Custom Claim'}</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-300 font-mono mt-0.5">
|
||||
Value: <span className="text-emerald-400">{claim.value}</span>
|
||||
{claim.issuer && <span className="text-slate-500 ml-2">({claim.issuer})</span>}
|
||||
(Array.isArray(userClaims) ? userClaims : []).map((claim, idx) => {
|
||||
if (!claim) return null;
|
||||
const claimType = claim.claimType || claim.type || 'Custom Claim';
|
||||
const claimVal = claim.claimValue ?? claim.value ?? '';
|
||||
const claimId = claim.claimId || (claim as any).id || claimType || String(idx);
|
||||
return (
|
||||
<div key={idx} className="p-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-white font-medium flex items-center gap-1.5">
|
||||
<Key className="w-3.5 h-3.5 text-amber-400" />
|
||||
<span>{claimType}</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-300 font-mono mt-0.5">
|
||||
Value: <span className="text-emerald-400">{claimVal}</span>
|
||||
{claim.issuer && <span className="text-slate-500 ml-2">({claim.issuer})</span>}
|
||||
</div>
|
||||
</div>
|
||||
{claimType && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteClaimMutation.mutate(claimId)}
|
||||
disabled={deleteClaimMutation.isPending || !canManageRolesAndClaims}
|
||||
className="p-1.5 text-rose-400 hover:text-rose-300 hover:bg-rose-500/10 rounded-lg transition disabled:opacity-50 cursor-pointer"
|
||||
title="Delete Claim"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{claim.type && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteClaimMutation.mutate(claim.type || String(idx))}
|
||||
disabled={deleteClaimMutation.isPending}
|
||||
className="p-1.5 text-rose-400 hover:text-rose-300 hover:bg-rose-500/10 rounded-lg transition"
|
||||
title="Delete Claim"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user