Add disconnect/uninstall for persistent sessions
- Server: Add DELETE /api/sessions/:id endpoint to disconnect agents - Server: SessionManager.disconnect_session() sends Disconnect message - Agent: Handle ADMIN_DISCONNECT to trigger uninstall - Agent: Add startup::uninstall() to remove from startup and schedule exe deletion - Dashboard: Add Disconnect button in Access tab machine details - Dashboard: Add Chat button for persistent sessions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,7 @@ async fn main() -> Result<()> {
|
||||
// REST API - Sessions
|
||||
.route("/api/sessions", get(list_sessions))
|
||||
.route("/api/sessions/:id", get(get_session))
|
||||
.route("/api/sessions/:id", axum::routing::delete(disconnect_session))
|
||||
|
||||
// HTML page routes (clean URLs)
|
||||
.route("/login", get(serve_login))
|
||||
@@ -178,6 +179,23 @@ async fn get_session(
|
||||
Ok(Json(api::SessionInfo::from(session)))
|
||||
}
|
||||
|
||||
async fn disconnect_session(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let session_id = match uuid::Uuid::parse_str(&id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid session ID"),
|
||||
};
|
||||
|
||||
if state.sessions.disconnect_session(session_id, "Disconnected by administrator").await {
|
||||
info!("Session {} disconnected by admin", session_id);
|
||||
(StatusCode::OK, "Session disconnected")
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "Session not found")
|
||||
}
|
||||
}
|
||||
|
||||
// Static page handlers
|
||||
async fn serve_login() -> impl IntoResponse {
|
||||
match tokio::fs::read_to_string("static/login.html").await {
|
||||
|
||||
@@ -134,6 +134,32 @@ impl SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnect a session by sending a disconnect message to the agent
|
||||
/// Returns true if the message was sent successfully
|
||||
pub async fn disconnect_session(&self, session_id: SessionId, reason: &str) -> bool {
|
||||
let sessions = self.sessions.read().await;
|
||||
if let Some(session_data) = sessions.get(&session_id) {
|
||||
// Create disconnect message
|
||||
use crate::proto;
|
||||
use prost::Message;
|
||||
|
||||
let disconnect_msg = proto::Message {
|
||||
payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
|
||||
reason: reason.to_string(),
|
||||
})),
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
if disconnect_msg.encode(&mut buf).is_ok() {
|
||||
// Send via input channel (will be forwarded to agent's WebSocket)
|
||||
if session_data.input_tx.send(buf).await.is_ok() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// List all active sessions
|
||||
pub async fn list_sessions(&self) -> Vec<Session> {
|
||||
let sessions = self.sessions.read().await;
|
||||
|
||||
@@ -709,7 +709,9 @@
|
||||
'<div class="detail-section">' +
|
||||
'<div class="detail-section-title">Actions</div>' +
|
||||
'<button class="btn btn-primary" style="width: 100%; margin-bottom: 8px;" onclick="connectToMachine(\'' + m.id + '\')">Connect</button>' +
|
||||
'<button class="btn btn-outline" style="width: 100%;" disabled>Transfer Files</button>' +
|
||||
'<button class="btn btn-outline" style="width: 100%; margin-bottom: 8px;" onclick="openChat(\'' + m.id + '\', \'' + (m.agent_name || 'Client').replace(/'/g, "\\'") + '\')">Chat</button>' +
|
||||
'<button class="btn btn-outline" style="width: 100%; margin-bottom: 8px;" disabled>Transfer Files</button>' +
|
||||
'<button class="btn btn-outline" style="width: 100%; color: hsl(0, 62.8%, 50%);" onclick="disconnectMachine(\'' + m.id + '\', \'' + (m.agent_name || m.agent_id).replace(/'/g, "\\'") + '\')">Disconnect</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -718,6 +720,22 @@
|
||||
alert("Viewer not yet implemented.\\n\\nSession ID: " + sessionId + "\\n\\nWebSocket: wss://connect.azcomputerguru.com/ws/viewer?session_id=" + sessionId);
|
||||
}
|
||||
|
||||
async function disconnectMachine(sessionId, machineName) {
|
||||
if (!confirm("Disconnect " + machineName + "?\\n\\nThis will end the remote session.")) return;
|
||||
try {
|
||||
const response = await fetch("/api/sessions/" + sessionId, { method: "DELETE" });
|
||||
if (response.ok) {
|
||||
selectedMachine = null;
|
||||
renderMachineDetail();
|
||||
loadMachines();
|
||||
} else {
|
||||
alert("Failed to disconnect: " + await response.text());
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Error disconnecting machine");
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh machines every 5 seconds
|
||||
loadMachines();
|
||||
setInterval(loadMachines, 5000);
|
||||
|
||||
Reference in New Issue
Block a user