Removed requeets form netweorks tab
Actiev sessions now refreshes every 5 seconds Better session mgmt
This commit is contained in:
+102
-15
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user