Fixed Console user lockout and unlock implementation #165

Merged
khwezi merged 1 commits from finishline into main 2026-10-24 14:39:41 +02:00
6 changed files with 78 additions and 26 deletions
@@ -237,16 +237,26 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
onError: (err: unknown) => toastError('Logout Failed', err),
});
const lockoutMutation = useMutation({
mutationFn: (disableLock: boolean) => setUserLockout(userId, disableLock),
onSuccess: (_, disableLock) => {
success(
disableLock ? 'Account Unlocked' : 'Account Locked',
`User account lockout status successfully modified.`
);
const lockUserMutation = useMutation({
mutationFn: () => setUserLockout(userId, true),
onSuccess: () => {
success('Account Locked', 'User account lockout enabled (disable = true).');
refetchUser();
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['user-detail', userId] });
},
onError: (err: unknown) => toastError('Lockout Update Failed', err),
onError: (err: unknown) => toastError('Lock User Failed', err),
});
const unlockUserMutation = useMutation({
mutationFn: () => setUserLockout(userId, false),
onSuccess: () => {
success('Account Unlocked', 'User account lockout disabled (disable = false).');
refetchUser();
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['user-detail', userId] });
},
onError: (err: unknown) => toastError('Unlock User Failed', err),
});
const statusMutation = useMutation({
@@ -643,19 +653,27 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => lockoutMutation.mutate(false)}
disabled={lockoutMutation.isPending || !canManageSessions}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
lockUserMutation.mutate();
}}
disabled={lockUserMutation.isPending || unlockUserMutation.isPending || !canManageSessions}
className="px-3 py-1.5 bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 rounded-xl font-semibold transition cursor-pointer disabled:opacity-50"
>
Lock User
{lockUserMutation.isPending ? 'Locking...' : 'Lock User'}
</button>
<button
type="button"
onClick={() => lockoutMutation.mutate(true)}
disabled={lockoutMutation.isPending || !canManageSessions}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
unlockUserMutation.mutate();
}}
disabled={unlockUserMutation.isPending || lockUserMutation.isPending || !canManageSessions}
className="px-3 py-1.5 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/20 rounded-xl font-semibold transition cursor-pointer disabled:opacity-50"
>
Unlock User
{unlockUserMutation.isPending ? 'Unlocking...' : 'Unlock User'}
</button>
</div>
</div>
@@ -524,7 +524,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}
setIsPendingActivation(true);
setIsSynced(false);
setActivationError(syncErr?.message || 'Account activation pending');
const rawMsg = syncErr?.message || 'Account activation pending';
const cleanMsg = /network\s*error/i.test(rawMsg) ? rawMsg.replace(/network\s*error/gi, 'Permission Check') : rawMsg;
setActivationError(cleanMsg);
}
setIsLoading(false);
@@ -655,7 +657,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}
setIsPendingActivation(true);
setIsSynced(false);
setActivationError(syncErr?.message || 'Account activation pending');
const rawMsg = syncErr?.message || 'Account activation pending';
const cleanMsg = /network\s*error/i.test(rawMsg) ? rawMsg.replace(/network\s*error/gi, 'Permission Check') : rawMsg;
setActivationError(cleanMsg);
} finally {
setIsLoading(false);
}
@@ -741,7 +745,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
await logout();
return { success: false, isUnauthorized: true };
}
const msg = err?.message || 'Sync failed';
const rawMsg = err?.message || 'Sync failed';
const msg = /network\s*error/i.test(rawMsg) ? rawMsg.replace(/network\s*error/gi, 'Permission Check') : rawMsg;
setIsPendingActivation(true);
setIsSynced(false);
setActivationError(msg);
@@ -22,6 +22,15 @@ interface AccountActivationModalProps {
initialError?: string | null;
}
const formatLockoutMessage = (msg?: string | null): string | null => {
if (!msg) return null;
const trimmed = msg.trim();
if (/^network\s*error$/i.test(trimmed)) {
return 'Permission Check';
}
return trimmed.replace(/network\s*error/gi, 'Permission Check');
};
export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
user,
claims,
@@ -31,11 +40,11 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
}) => {
const navigate = useNavigate();
const [isSyncing, setIsSyncing] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(initialError || null);
const [errorMessage, setErrorMessage] = useState<string | null>(formatLockoutMessage(initialError) || null);
useEffect(() => {
if (initialError !== undefined) {
setErrorMessage(initialError);
setErrorMessage(formatLockoutMessage(initialError));
}
}, [initialError]);
@@ -68,8 +77,10 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
if (!result.success) {
setErrorMessage(
result.errorMessage ||
'Activation is still being reviewed by your funding administrator. Please check back shortly.'
formatLockoutMessage(
result.errorMessage ||
'Activation is still being reviewed by your funding administrator. Please check back shortly.'
)
);
}
} catch (err: unknown) {
@@ -79,7 +90,7 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
navigate('/', { replace: true });
return;
}
setErrorMessage('Unable to verify activation right now. Please try again in a few moments.');
setErrorMessage('Permission Check: Unable to verify activation right now. Please try again in a few moments.');
} finally {
setIsSyncing(false);
}
@@ -143,7 +154,7 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
{errorMessage && (
<div className="w-full rounded-2xl bg-amber-500/10 border border-amber-500/20 p-3 text-xs text-amber-300/90 leading-relaxed text-center flex items-center justify-center gap-2">
<Sparkles className="w-4 h-4 text-amber-400 shrink-0" />
<span>{errorMessage}</span>
<span>{formatLockoutMessage(errorMessage)}</span>
</div>
)}
@@ -115,7 +115,9 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
</div>
<p className="text-sm text-slate-300 leading-relaxed max-w-sm">
{reason || 'Your security credentials have expired for your protection. Please sign in again to continue.'}
{reason
? reason.replace(/network\s*error/gi, 'Permission Check')
: 'Your security credentials have expired for your protection. Please sign in again to continue.'}
</p>
{/* Smooth Countdown Bar */}
@@ -157,12 +157,20 @@ export async function verifyTenantLockout(
}
if (!tenantRes || (typeof tenantRes === 'object' && Object.keys(tenantRes).length === 0)) {
const serverMsg =
let serverMsg =
lastError?.response?.data?.detail ||
lastError?.response?.data?.message ||
lastError?.message ||
'No tenant record returned';
if (typeof serverMsg === 'string') {
if (/^network\s*error$/i.test(serverMsg.trim())) {
serverMsg = 'Permission Check';
} else {
serverMsg = serverMsg.replace(/network\s*error/gi, 'Permission Check');
}
}
const errorMsg = `Tenant record could not be retrieved (${serverMsg}). Account is locked out.`;
setActivationPending(true, errorMsg);
return {
@@ -263,13 +263,21 @@ export async function syncUserAccount(
}
clearSuccessfulSyncRecord();
const serverMsg =
let serverMsg =
err?.response?.data?.detail ||
err?.response?.data?.message ||
err?.response?.data?.error ||
err?.message ||
'Account is not yet enabled by the administrator.';
if (typeof serverMsg === 'string') {
if (/^network\s*error$/i.test(serverMsg.trim())) {
serverMsg = 'Permission Check';
} else {
serverMsg = serverMsg.replace(/network\s*error/gi, 'Permission Check');
}
}
setActivationPending(true, serverMsg);
return {