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), onError: (err: unknown) => toastError('Logout Failed', err),
}); });
const lockoutMutation = useMutation({ const lockUserMutation = useMutation({
mutationFn: (disableLock: boolean) => setUserLockout(userId, disableLock), mutationFn: () => setUserLockout(userId, true),
onSuccess: (_, disableLock) => { onSuccess: () => {
success( success('Account Locked', 'User account lockout enabled (disable = true).');
disableLock ? 'Account Unlocked' : 'Account Locked',
`User account lockout status successfully modified.`
);
refetchUser(); 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({ const statusMutation = useMutation({
@@ -643,19 +653,27 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => lockoutMutation.mutate(false)} onClick={(e) => {
disabled={lockoutMutation.isPending || !canManageSessions} 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" 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>
<button <button
type="button" type="button"
onClick={() => lockoutMutation.mutate(true)} onClick={(e) => {
disabled={lockoutMutation.isPending || !canManageSessions} 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" 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> </button>
</div> </div>
</div> </div>
@@ -524,7 +524,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
} }
setIsPendingActivation(true); setIsPendingActivation(true);
setIsSynced(false); 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); setIsLoading(false);
@@ -655,7 +657,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
} }
setIsPendingActivation(true); setIsPendingActivation(true);
setIsSynced(false); 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 { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -741,7 +745,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
await logout(); await logout();
return { success: false, isUnauthorized: true }; 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); setIsPendingActivation(true);
setIsSynced(false); setIsSynced(false);
setActivationError(msg); setActivationError(msg);
@@ -22,6 +22,15 @@ interface AccountActivationModalProps {
initialError?: string | null; 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> = ({ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
user, user,
claims, claims,
@@ -31,11 +40,11 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(initialError || null); const [errorMessage, setErrorMessage] = useState<string | null>(formatLockoutMessage(initialError) || null);
useEffect(() => { useEffect(() => {
if (initialError !== undefined) { if (initialError !== undefined) {
setErrorMessage(initialError); setErrorMessage(formatLockoutMessage(initialError));
} }
}, [initialError]); }, [initialError]);
@@ -68,8 +77,10 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
if (!result.success) { if (!result.success) {
setErrorMessage( setErrorMessage(
result.errorMessage || formatLockoutMessage(
'Activation is still being reviewed by your funding administrator. Please check back shortly.' result.errorMessage ||
'Activation is still being reviewed by your funding administrator. Please check back shortly.'
)
); );
} }
} catch (err: unknown) { } catch (err: unknown) {
@@ -79,7 +90,7 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
navigate('/', { replace: true }); navigate('/', { replace: true });
return; 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 { } finally {
setIsSyncing(false); setIsSyncing(false);
} }
@@ -143,7 +154,7 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
{errorMessage && ( {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"> <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" /> <Sparkles className="w-4 h-4 text-amber-400 shrink-0" />
<span>{errorMessage}</span> <span>{formatLockoutMessage(errorMessage)}</span>
</div> </div>
)} )}
@@ -115,7 +115,9 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
</div> </div>
<p className="text-sm text-slate-300 leading-relaxed max-w-sm"> <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> </p>
{/* Smooth Countdown Bar */} {/* Smooth Countdown Bar */}
@@ -157,12 +157,20 @@ export async function verifyTenantLockout(
} }
if (!tenantRes || (typeof tenantRes === 'object' && Object.keys(tenantRes).length === 0)) { if (!tenantRes || (typeof tenantRes === 'object' && Object.keys(tenantRes).length === 0)) {
const serverMsg = let serverMsg =
lastError?.response?.data?.detail || lastError?.response?.data?.detail ||
lastError?.response?.data?.message || lastError?.response?.data?.message ||
lastError?.message || lastError?.message ||
'No tenant record returned'; '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.`; const errorMsg = `Tenant record could not be retrieved (${serverMsg}). Account is locked out.`;
setActivationPending(true, errorMsg); setActivationPending(true, errorMsg);
return { return {
@@ -263,13 +263,21 @@ export async function syncUserAccount(
} }
clearSuccessfulSyncRecord(); clearSuccessfulSyncRecord();
const serverMsg = let serverMsg =
err?.response?.data?.detail || err?.response?.data?.detail ||
err?.response?.data?.message || err?.response?.data?.message ||
err?.response?.data?.error || err?.response?.data?.error ||
err?.message || err?.message ||
'Account is not yet enabled by the administrator.'; '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); setActivationPending(true, serverMsg);
return { return {