Implemented session and account lockout management
Enabled account sync control and session expiration management Applied some fixes to the govername page and project page
This commit is contained in:
@@ -27,6 +27,8 @@ import {
|
||||
getUser,
|
||||
updateUserStatus,
|
||||
syncIntegration,
|
||||
syncUser,
|
||||
getUserSyncRecord,
|
||||
getInvites,
|
||||
sendInvite,
|
||||
getApplicantInvites,
|
||||
@@ -38,6 +40,7 @@ import { Modal } from '../components/ui/Modal';
|
||||
import { useToast } from '../components/ui/Toast';
|
||||
import { useAuth } from '../auth/AuthProvider';
|
||||
import { getAppConfig } from '../config';
|
||||
import { triggerSessionExpired } from '../services/sessionExpiration';
|
||||
import {
|
||||
EmailStatuses,
|
||||
NotificationStatus,
|
||||
@@ -98,13 +101,13 @@ export const SettingsPage: React.FC = () => {
|
||||
});
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: () => syncIntegration(),
|
||||
mutationFn: () => syncUser(undefined, true),
|
||||
onSuccess: () => {
|
||||
success('Integration Sync Complete', 'Background user and tenant directory synchronized successfully.');
|
||||
success('User Sync Complete', 'Authenticated user enabled and directory synced. Browser cache updated to prevent endpoint hotspot.');
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toastError('Sync Failed', err?.message || 'Failed to trigger background integration sync.');
|
||||
toastError('Sync Failed', err);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -119,7 +122,7 @@ export const SettingsPage: React.FC = () => {
|
||||
}
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toastError('Update Failed', err?.message || 'Could not update user status.');
|
||||
toastError('Update Failed', err);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -170,7 +173,7 @@ export const SettingsPage: React.FC = () => {
|
||||
setInviteMessage('');
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toastError('Dispatch Error', err?.message || 'Failed to dispatch invitation.');
|
||||
toastError('Dispatch Error', err);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -183,7 +186,7 @@ export const SettingsPage: React.FC = () => {
|
||||
setApplicantInvitesResults(res);
|
||||
success('Lookup Complete', `Retrieved ${res.length} invitation records for applicant.`);
|
||||
} catch (err: any) {
|
||||
toastError('Lookup Error', err?.message || 'Applicant invitation lookup failed.');
|
||||
toastError('Lookup Error', err);
|
||||
setApplicantInvitesResults([]);
|
||||
} finally {
|
||||
setSearchingApplicant(false);
|
||||
@@ -224,14 +227,24 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{activeTab === 'users' && (
|
||||
<button
|
||||
onClick={() => syncMutation.mutate()}
|
||||
disabled={syncMutation.isPending}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 border border-slate-700/80 rounded-xl shadow-md transition cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${syncMutation.isPending ? 'animate-spin' : ''}`} />
|
||||
{syncMutation.isPending ? 'Syncing...' : 'Sync Integration'}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
|
||||
title={getUserSyncRecord() ? `Endpoint: ${getUserSyncRecord()?.syncedEndpoint || '/v1/api/users/sync'} (Synced in browser)` : 'Sync active in browser'}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
Browser Sync Saved
|
||||
</span>
|
||||
<button
|
||||
onClick={() => syncMutation.mutate()}
|
||||
disabled={syncMutation.isPending}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 border border-slate-700/80 rounded-xl shadow-md transition cursor-pointer disabled:opacity-50"
|
||||
title="Force refresh user sync and update browser cache"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${syncMutation.isPending ? 'animate-spin' : ''}`} />
|
||||
{syncMutation.isPending ? 'Syncing...' : 'Sync User Directory'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'invites' && (
|
||||
@@ -366,8 +379,8 @@ export const SettingsPage: React.FC = () => {
|
||||
<table className="w-full text-left text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
||||
<th className="py-3.5 px-5">User ID (UUID)</th>
|
||||
<th className="py-3.5 px-4">Email</th>
|
||||
<th className="py-3.5 px-5">User Email</th>
|
||||
<th className="py-3.5 px-4">User Identifier</th>
|
||||
<th className="py-3.5 px-4">Status</th>
|
||||
<th className="py-3.5 px-4">Last Login</th>
|
||||
<th className="py-3.5 px-5 text-right">Actions</th>
|
||||
@@ -391,11 +404,11 @@ export const SettingsPage: React.FC = () => {
|
||||
const badge = getActivityStatusBadge(u.status);
|
||||
return (
|
||||
<tr key={u.id} className="hover:bg-slate-800/40 transition-colors">
|
||||
<td className="py-3.5 px-5 font-mono text-slate-300 text-[11px]">
|
||||
{u.id}
|
||||
<td className="py-3.5 px-5 text-white font-medium">
|
||||
{u.email || getUserEmail(u.id) || <span className="text-slate-500 italic">No email recorded</span>}
|
||||
</td>
|
||||
<td className="py-3.5 px-4 text-white font-medium">
|
||||
{u.email || <span className="text-slate-500 italic">No email recorded</span>}
|
||||
<td className="py-3.5 px-4 font-mono text-slate-400 text-[11px]">
|
||||
{u.id}
|
||||
</td>
|
||||
<td className="py-3.5 px-4">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold border ${badge.className}`}>
|
||||
@@ -778,6 +791,31 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-6 rounded-2xl border border-slate-800/80 shadow-xl space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-amber-400" />
|
||||
Session Expiration vs Lockout Architecture (HTTP 401)
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-0.5 max-w-2xl leading-relaxed">
|
||||
To disambiguate expired sessions from the session lockout process, the session expiration handler monitors all active business endpoints for HTTP 401 other than the user sync endpoint. The user sync endpoint remains dedicated to the session lockout & activation check, ensuring both features act in harmony where one is subordinated by call order and priority.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="simulate-401-btn"
|
||||
onClick={() => {
|
||||
triggerSessionExpired('Simulated HTTP 401: Security token expired on business endpoint during active session test.');
|
||||
}}
|
||||
className="inline-flex items-center gap-2 px-3.5 py-2 text-xs font-semibold text-amber-300 bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 rounded-xl transition-all shadow-md shrink-0"
|
||||
>
|
||||
<AlertCircle className="w-3.5 h-3.5 text-amber-400" />
|
||||
Simulate HTTP 401 Session Expiry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -939,12 +977,12 @@ export const SettingsPage: React.FC = () => {
|
||||
<div className="space-y-4 text-xs">
|
||||
<div className="p-4 bg-slate-900/60 rounded-xl border border-slate-800 space-y-2">
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User UUID:</span>
|
||||
<span className="font-mono text-white text-xs break-all">{selectedUserForDetail.id}</span>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User Email:</span>
|
||||
<span className="text-white font-semibold text-xs">{selectedUserForDetail.email || getUserEmail(selectedUserForDetail.id) || 'Not specified'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Email:</span>
|
||||
<span className="text-white">{selectedUserForDetail.email || 'Not specified'}</span>
|
||||
<span className="text-slate-400 font-semibold block text-[11px] uppercase">User Identifier:</span>
|
||||
<span className="font-mono text-slate-300 text-xs break-all">{selectedUserForDetail.id}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user