Removed requeets form netweorks tab

Actiev sessions now refreshes every 5 seconds
Better session mgmt
This commit is contained in:
2026-05-29 06:28:26 +00:00
parent a13e298d8a
commit fe0b114ebf
4 changed files with 232 additions and 183 deletions
+102 -15
View File
@@ -57,7 +57,7 @@ import {
api,
ApiError,
UserNetworkApproval,
ActivationSession,
AdminSession,
KillSwitchEvent,
PortalNetwork,
OrganizationMember,
@@ -114,7 +114,7 @@ export default function AccessPage() {
const [approvals, setApprovals] = useState<UserNetworkApproval[]>([]);
const [pendingApprovals, setPendingApprovals] = useState<UserNetworkApproval[]>([]);
const [sessions, setSessions] = useState<ActivationSession[]>([]);
const [sessions, setSessions] = useState<AdminSession[]>([]);
const [killSwitchEvents, setKillSwitchEvents] = useState<KillSwitchEvent[]>([]);
const [networks, setNetworks] = useState<PortalNetwork[]>([]);
const [orgMembers, setOrgMembers] = useState<OrganizationMember[]>([]);
@@ -143,7 +143,8 @@ export default function AccessPage() {
const [killError, setKillError] = useState<string | null>(null);
const [endSessionId, setEndSessionId] = useState<string | null>(null);
const [isEndingSession, setIsEndingSession] = useState(false);
const [showEndSessionConfirm, setShowEndSessionConfirm] = useState(false);
const [endSessionTarget, setEndSessionTarget] = useState<AdminSession | null>(null);
const [selectedApproval, setSelectedApproval] = useState<UserNetworkApproval | null>(null);
const [allMemberships, setAllMemberships] = useState<EnrichedMembership[]>([]);
@@ -164,7 +165,7 @@ export default function AccessPage() {
const [pendingRes, allApprovalsRes, sessionsRes, networksRes, membersRes, allMemsRes] = await Promise.allSettled([
api.zerotier.listPendingApprovals(orgId),
api.zerotier.adminListAllApprovals(orgId),
api.zerotier.listSessions(orgId),
api.zerotier.adminListSessions(orgId),
api.zerotier.listNetworks(orgId),
api.organizations.getMembers(orgId),
api.zerotier.adminListAllMemberships(orgId),
@@ -188,6 +189,21 @@ export default function AccessPage() {
fetchData();
}, [fetchData]);
const refreshSessions = useCallback(async () => {
if (!orgId) return;
try {
const sessionsRes = await api.zerotier.adminListSessions(orgId);
setSessions(sessionsRes.sessions || []);
} catch {
// silent
}
}, [orgId]);
useEffect(() => {
const interval = setInterval(() => refreshSessions(), 5000);
return () => clearInterval(interval);
}, [refreshSessions]);
const handleApprove = async (approvalId: string) => {
if (!orgId) return;
setApproveId(approvalId);
@@ -278,18 +294,42 @@ export default function AccessPage() {
}
};
const handleEndSession = async (sessionId: string) => {
if (!orgId) return;
const handleEndSession = (session: AdminSession) => {
setEndSessionTarget(session);
setShowEndSessionConfirm(true);
};
const handleEndSessionConfirm = async () => {
if (!orgId || !endSessionTarget) return;
const sessionId = endSessionTarget.id;
setEndSessionId(sessionId);
setIsEndingSession(true);
setShowEndSessionConfirm(false);
try {
await api.zerotier.endSession(orgId, sessionId);
toast({ title: "Session ended" });
fetchData();
const res = await api.zerotier.adminEndSession(orgId, sessionId);
setSessions((prev) =>
prev.map((s) =>
s.id === sessionId
? { ...s, ended_at: res.session.ended_at, is_expired: true, is_active: false }
: s
)
);
toast({ title: "Session ended", description: res.message });
} catch (err) {
toast({ variant: "destructive", title: "Failed to end session", description: err instanceof ApiError ? err.message : "Something went wrong." });
if (err instanceof ApiError) {
if (err.message?.includes("NOT_FOUND")) {
toast({ variant: "destructive", title: "Session not found", description: `Session ${sessionId} not found.` });
} else if (err.message?.includes("already ended")) {
toast({ variant: "destructive", title: "Session already ended", description: "This session has already been ended." });
} else {
toast({ variant: "destructive", title: "Failed to end session", description: err.message });
}
} else {
toast({ variant: "destructive", title: "Failed to end session", description: "Something went wrong." });
}
fetchData();
} finally {
setEndSessionId(null);
setEndSessionTarget(null);
}
};
@@ -510,12 +550,34 @@ export default function AccessPage() {
<Zap className="w-4 h-4 text-green-500" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium font-mono truncate">{session.device_network_membership_id}</p>
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<p className="font-medium truncate">{session.user?.full_name || session.user?.email || "Unknown user"}</p>
{session.user?.email && (
<p className="text-xs text-muted-foreground">{session.user.email}</p>
)}
<div className="flex items-center gap-2 mt-1 flex-wrap">
{session.device && (
<Badge variant="outline" className="text-xs font-mono">
{session.device.name || session.device.node_id}
</Badge>
)}
{session.network && (
<Badge variant="secondary" className="text-xs">
{session.network.name}
</Badge>
)}
</div>
<div className="flex items-center gap-3 text-sm text-muted-foreground mt-1">
<span>Activated: {formatDate(session.authenticated_at)}</span>
<span className="text-green-600 font-medium flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatExpiry(session.expires_at)}
{session.remaining_seconds > 0
? (() => {
const min = Math.floor(session.remaining_seconds / 60);
return min >= 60
? `${Math.floor(min / 60)}h ${min % 60}m remaining`
: `${min}m remaining`;
})()
: "Expired"}
</span>
</div>
</div>
@@ -523,7 +585,7 @@ export default function AccessPage() {
size="sm"
variant="outline"
className="text-orange-600 border-orange-300 hover:bg-orange-50 gap-1 flex-shrink-0"
onClick={() => handleEndSession(session.id)}
onClick={() => handleEndSession(session)}
disabled={endSessionId === session.id}
>
{endSessionId === session.id ? <Loader2 className="w-3 h-3 animate-spin" /> : <ZapOff className="w-3 h-3" />}
@@ -835,6 +897,31 @@ export default function AccessPage() {
</DialogFooter>
</DialogContent>
</Dialog>
{/* End Session Confirmation Dialog */}
<Dialog open={showEndSessionConfirm} onOpenChange={(open) => { if (!open) { setShowEndSessionConfirm(false); setEndSessionTarget(null); } }}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>End Session</DialogTitle>
<DialogDescription>
End session for <strong>{endSessionTarget?.user?.full_name || endSessionTarget?.user?.email || "this user"}</strong> on <strong>{endSessionTarget?.network?.name || "this network"}</strong>? They will need to re-authenticate.
</DialogDescription>
</DialogHeader>
<div className="p-3 border border-orange-300 rounded-lg bg-orange-50 text-sm text-orange-800">
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>The user's approval will NOT be revoked they can re-authenticate without admin re-approval. The device will be deauthorized from the ZeroTier network and lose connectivity until they re-authenticate.</span>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => { setShowEndSessionConfirm(false); setEndSessionTarget(null); }}>Cancel</Button>
<Button variant="destructive" onClick={handleEndSessionConfirm}>
<ZapOff className="w-4 h-4 mr-2" />
End Session
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}