Overhauled lockout process

This commit is contained in:
2026-10-24 17:36:30 +02:00
parent a731c979bb
commit 25e251aa79
12 changed files with 574 additions and 527 deletions
@@ -962,6 +962,7 @@ export const TenantManagementPage: React.FC = () => {
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
title="Provision New Tenant Partition"
subtitle="Define an isolated multi-tenant partition for grants, portfolios, and users."
size="md"
>
<form
@@ -970,17 +971,6 @@ export const TenantManagementPage: React.FC = () => {
)}
className="space-y-4"
>
<div className="p-3.5 rounded-xl bg-blue-950/20 border border-blue-800/30 text-xs text-blue-200 flex items-start gap-2.5">
<Info className="w-4 h-4 text-blue-400 shrink-0 mt-0.5" />
<div className="space-y-1 leading-relaxed">
<p className="font-semibold text-white">Multi-Tenant Partition Boundary</p>
<p>
Creating a new tenant defines an isolated partition boundary for grants, portfolios, projects, and users.
New tenants are automatically initialized in the Active state.
</p>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Tenant Organization Name <span className="text-rose-400">*</span>
@@ -1007,7 +997,7 @@ export const TenantManagementPage: React.FC = () => {
<button
type="submit"
disabled={createMutation.isPending}
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white text-xs font-semibold rounded-xl shadow-md transition disabled:opacity-50 cursor-pointer"
className="flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold rounded-xl shadow-md transition disabled:opacity-50 cursor-pointer"
>
{createMutation.isPending && <RefreshCw className="w-3.5 h-3.5 animate-spin" />}
<span>{createMutation.isPending ? 'Provisioning...' : 'Create Tenant'}</span>
@@ -1021,6 +1011,7 @@ export const TenantManagementPage: React.FC = () => {
isOpen={isUpdateModalOpen}
onClose={() => setIsUpdateModalOpen(false)}
title={`Update Tenant: ${selectedTenant?.name || ''}`}
subtitle="Modify tenant partition name and operational access status."
size="md"
>
{selectedTenant && (
@@ -1064,13 +1055,12 @@ export const TenantManagementPage: React.FC = () => {
{/* Warning when selecting Inactive or Disabled */}
{Number(updateForm.watch('status')) !== ActivityStatus.Active && (
<div className="p-3 rounded-xl bg-amber-950/30 border border-amber-800/40 text-xs text-amber-200 flex items-start gap-2.5">
<div className="p-3 rounded-xl bg-amber-500/10 border border-amber-500/20 text-xs text-amber-200 flex items-start gap-2.5">
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="font-semibold text-white">Multitenancy Lockout Warning</p>
<p className="text-[11px] leading-relaxed text-amber-300/90">
Changing this tenant's status to Inactive or Disabled immediately locks out all users assigned to this partition.
The backend API will deny requests until the tenant is restored to Active status.
<div className="space-y-0.5">
<p className="font-semibold text-amber-300">Lockout Notice</p>
<p className="text-[11px] text-amber-200/90 leading-relaxed">
Setting this tenant to Inactive or Disabled immediately locks out assigned users from accessing the API.
</p>
</div>
</div>
@@ -1087,7 +1077,7 @@ export const TenantManagementPage: React.FC = () => {
<button
type="submit"
disabled={updateMutation.isPending}
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white text-xs font-semibold rounded-xl shadow-md transition disabled:opacity-50 cursor-pointer"
className="flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold rounded-xl shadow-md transition disabled:opacity-50 cursor-pointer"
>
{updateMutation.isPending && <RefreshCw className="w-3.5 h-3.5 animate-spin" />}
<span>{updateMutation.isPending ? 'Saving...' : 'Update Tenant'}</span>
@@ -1102,66 +1092,47 @@ export const TenantManagementPage: React.FC = () => {
isOpen={isDetailModalOpen}
onClose={() => setIsDetailModalOpen(false)}
title={`Tenant Details: ${selectedTenant?.name || ''}`}
subtitle="Partition boundary information, creation audit, and operational status."
size="lg"
>
{selectedTenant && (
<div className="space-y-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-slate-950/60 p-4 rounded-xl border border-slate-800">
<div className="space-y-4">
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 bg-slate-950/60 p-3.5 rounded-xl border border-slate-800 text-xs">
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Tenant Numeric ID</p>
<p className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Tenant ID</p>
<p className="text-sm font-bold text-white mt-0.5">#{selectedTenant.id}</p>
</div>
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Tenant Name</p>
<p className="text-sm font-semibold text-slate-200 mt-0.5">{selectedTenant.name || '—'}</p>
<p className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Tenant Name</p>
<p className="text-xs font-semibold text-slate-200 mt-0.5 truncate">{selectedTenant.name || '—'}</p>
</div>
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Operational Status</p>
<p className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Operational Status</p>
<div className="mt-1">
<ActivityStatusBadge status={selectedTenant.status} />
</div>
</div>
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Created Date</p>
<p className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Created Date</p>
<p className="text-xs text-slate-300 mt-0.5">{formatDate(selectedTenant.createdAt)}</p>
</div>
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Created By</p>
<p className="text-xs font-mono text-slate-300 mt-0.5">
<p className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Created By</p>
<p className="text-xs font-mono text-slate-300 mt-0.5 truncate">
{getUserEmail(selectedTenant.createdBy) || selectedTenant.createdBy || 'System Admin'}
</p>
</div>
{selectedTenant.updatedAt && (
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Last Updated</p>
<p className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Last Updated</p>
<p className="text-xs text-slate-300 mt-0.5">{formatDate(selectedTenant.updatedAt)}</p>
</div>
)}
{selectedTenant.updatedBy && (
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Updated By</p>
<p className="text-xs font-mono text-slate-300 mt-0.5">
{getUserEmail(selectedTenant.updatedBy) || selectedTenant.updatedBy}
</p>
</div>
)}
</div>
<div className="p-3.5 rounded-xl bg-slate-900/80 border border-slate-800 text-xs text-slate-300 space-y-2">
<p className="font-semibold text-white flex items-center gap-1.5">
<ShieldCheck className="w-4 h-4 text-blue-400" />
Partition Scope &amp; Authentication
</p>
<p className="text-[11px] text-slate-400 leading-relaxed">
When users authenticate, their security token contains claims identifying their tenant partition.
The system automatically scopes data queries to this tenant and enforces operational lockout if the status is not Active.
</p>
</div>
<div className="flex items-center justify-end gap-2.5 pt-3 border-t border-slate-800">
@@ -1171,7 +1142,7 @@ export const TenantManagementPage: React.FC = () => {
setIsDetailModalOpen(false);
handleOpenUpdate(selectedTenant);
}}
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white text-xs font-semibold rounded-xl transition cursor-pointer"
className="flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold rounded-xl transition cursor-pointer"
>
<Edit2 className="w-3.5 h-3.5" />
<span>Edit Tenant Status</span>