Merge pull request 'Fixed Console user lockout and unlock implementation' (#165) from finishline into main
Reviewed-on: #165
This commit was merged in pull request #165.
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user