Files
Mike Swanson 75ce1c2fd5 feat: Add Sequential Thinking to Code Review + Frontend Validation
Enhanced code review and frontend validation with intelligent triggers:

Code Review Agent Enhancement:
- Added Sequential Thinking MCP integration for complex issues
- Triggers on 2+ rejections or 3+ critical issues
- New escalation format with root cause analysis
- Comprehensive solution strategies with trade-off evaluation
- Educational feedback to break rejection cycles
- Files: .claude/agents/code-review.md (+308 lines)
- Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md

Frontend Design Skill Enhancement:
- Automatic invocation for ANY UI change
- Comprehensive validation checklist (200+ checkpoints)
- 8 validation categories (visual, interactive, responsive, a11y, etc.)
- 3 validation levels (quick, standard, comprehensive)
- Integration with code review workflow
- Files: .claude/skills/frontend-design/SKILL.md (+120 lines)
- Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines)

Settings Optimization:
- Repaired .claude/settings.local.json (fixed m365 pattern)
- Reduced permissions from 49 to 33 (33% reduction)
- Removed duplicates, sorted alphabetically
- Created SETTINGS_PERMISSIONS.md documentation

Checkpoint Command Enhancement:
- Dual checkpoint system (git + database)
- Saves session context to API for cross-machine recall
- Includes git metadata in database context
- Files: .claude/commands/checkpoint.md (+139 lines)

Decision Rationale:
- Sequential Thinking MCP breaks rejection cycles by identifying root causes
- Automatic frontend validation catches UI issues before code review
- Dual checkpoints enable complete project memory across machines
- Settings optimization improves maintainability

Total: 1,200+ lines of documentation and enhancements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 16:23:52 -07:00

488 lines
21 KiB
Plaintext

1→//! GuruConnect Server - WebSocket Relay Server
2→//!
3→//! Handles connections from both agents and dashboard viewers,
4→//! relaying video frames and input events between them.
5→
6→mod config;
7→mod relay;
8→mod session;
9→mod auth;
10→mod api;
11→mod db;
12→mod support_codes;
13→
14→pub mod proto {
15→ include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
16→}
17→
18→use anyhow::Result;
19→use axum::{
20→ Router,
21→<<<<<<< HEAD
22→ routing::{get, post, delete},
23→ extract::{Path, State, Json, Query},
24→=======
25→ routing::{get, post, put, delete},
26→ extract::{Path, State, Json, Request},
27→>>>>>>> b861cb1 (Add user management system with JWT authentication)
28→ response::{Html, IntoResponse},
29→ http::StatusCode,
30→ middleware::{self, Next},
31→};
32→use std::net::SocketAddr;
33→use std::sync::Arc;
34→use tower_http::cors::{Any, CorsLayer};
35→use tower_http::trace::TraceLayer;
36→use tower_http::services::ServeDir;
37→use tracing::{info, Level};
38→use tracing_subscriber::FmtSubscriber;
39→use serde::Deserialize;
40→
41→use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};
42→use auth::{JwtConfig, hash_password, generate_random_password};
43→
44→/// Application state
45→#[derive(Clone)]
46→pub struct AppState {
47→ sessions: session::SessionManager,
48→ support_codes: SupportCodeManager,
49→ db: Option<db::Database>,
50→ pub jwt_config: Arc<JwtConfig>,
51→}
52→
53→/// Middleware to inject JWT config into request extensions
54→async fn auth_layer(
55→ State(state): State<AppState>,
56→ mut request: Request,
57→ next: Next,
58→) -> impl IntoResponse {
59→ request.extensions_mut().insert(state.jwt_config.clone());
60→ next.run(request).await
61→}
62→
63→#[tokio::main]
64→async fn main() -> Result<()> {
65→ // Initialize logging
66→ let _subscriber = FmtSubscriber::builder()
67→ .with_max_level(Level::INFO)
68→ .with_target(true)
69→ .init();
70→
71→ info!("GuruConnect Server v{}", env!("CARGO_PKG_VERSION"));
72→
73→ // Load configuration
74→ let config = config::Config::load()?;
75→
76→ // Use port 3002 for GuruConnect
77→ let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
78→ info!("Loaded configuration, listening on {}", listen_addr);
79→
80→ // JWT configuration
81→ let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
82→ tracing::warn!("JWT_SECRET not set, using default (INSECURE for production!)");
83→ "guruconnect-dev-secret-change-me-in-production".to_string()
84→ });
85→ let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
86→ .ok()
87→ .and_then(|s| s.parse().ok())
88→ .unwrap_or(24i64);
89→ let jwt_config = Arc::new(JwtConfig::new(jwt_secret, jwt_expiry_hours));
90→
91→ // Initialize database if configured
92→ let database = if let Some(ref db_url) = config.database_url {
93→ match db::Database::connect(db_url, config.database_max_connections).await {
94→ Ok(db) => {
95→ // Run migrations
96→ if let Err(e) = db.migrate().await {
97→ tracing::error!("Failed to run migrations: {}", e);
98→ return Err(e);
99→ }
100→ Some(db)
101→ }
102→ Err(e) => {
103→ tracing::warn!("Failed to connect to database: {}. Running without persistence.", e);
104→ None
105→ }
106→ }
107→ } else {
108→ info!("No DATABASE_URL set, running without persistence");
109→ None
110→ };
111→
112→ // Create initial admin user if no users exist
113→ if let Some(ref db) = database {
114→ match db::count_users(db.pool()).await {
115→ Ok(0) => {
116→ info!("No users found, creating initial admin user...");
117→ let password = generate_random_password(16);
118→ let password_hash = hash_password(&password)?;
119→
120→ match db::create_user(db.pool(), "admin", &password_hash, None, "admin").await {
121→ Ok(user) => {
122→ // Set admin permissions
123→ let perms = vec![
124→ "view".to_string(),
125→ "control".to_string(),
126→ "transfer".to_string(),
127→ "manage_users".to_string(),
128→ "manage_clients".to_string(),
129→ ];
130→ let _ = db::set_user_permissions(db.pool(), user.id, &perms).await;
131→
132→ info!("========================================");
133→ info!(" INITIAL ADMIN USER CREATED");
134→ info!(" Username: admin");
135→ info!(" Password: {}", password);
136→ info!(" (Change this password after first login!)");
137→ info!("========================================");
138→ }
139→ Err(e) => {
140→ tracing::error!("Failed to create initial admin user: {}", e);
141→ }
142→ }
143→ }
144→ Ok(count) => {
145→ info!("{} user(s) in database", count);
146→ }
147→ Err(e) => {
148→ tracing::warn!("Could not check user count: {}", e);
149→ }
150→ }
151→ }
152→
153→ // Create session manager
154→ let sessions = session::SessionManager::new();
155→
156→ // Restore persistent machines from database
157→ if let Some(ref db) = database {
158→ match db::machines::get_all_machines(db.pool()).await {
159→ Ok(machines) => {
160→ info!("Restoring {} persistent machines from database", machines.len());
161→ for machine in machines {
162→ sessions.restore_offline_machine(&machine.agent_id, &machine.hostname).await;
163→ }
164→ }
165→ Err(e) => {
166→ tracing::warn!("Failed to restore machines: {}", e);
167→ }
168→ }
169→ }
170→
171→ // Create application state
172→ let state = AppState {
173→ sessions,
174→ support_codes: SupportCodeManager::new(),
175→ db: database,
176→ jwt_config,
177→ };
178→
179→ // Build router
180→ let app = Router::new()
181→ // Health check (no auth required)
182→ .route("/health", get(health))
183→
184→ // Auth endpoints (no auth required for login)
185→ .route("/api/auth/login", post(api::auth::login))
186→
187→ // Auth endpoints (auth required)
188→ .route("/api/auth/me", get(api::auth::get_me))
189→ .route("/api/auth/change-password", post(api::auth::change_password))
190→
191→ // User management (admin only)
192→ .route("/api/users", get(api::users::list_users))
193→ .route("/api/users", post(api::users::create_user))
194→ .route("/api/users/:id", get(api::users::get_user))
195→ .route("/api/users/:id", put(api::users::update_user))
196→ .route("/api/users/:id", delete(api::users::delete_user))
197→ .route("/api/users/:id/permissions", put(api::users::set_permissions))
198→ .route("/api/users/:id/clients", put(api::users::set_client_access))
199→
200→ // Portal API - Support codes
201→ .route("/api/codes", post(create_code))
202→ .route("/api/codes", get(list_codes))
203→ .route("/api/codes/:code/validate", get(validate_code))
204→ .route("/api/codes/:code/cancel", post(cancel_code))
205→
206→ // WebSocket endpoints
207→ .route("/ws/agent", get(relay::agent_ws_handler))
208→ .route("/ws/viewer", get(relay::viewer_ws_handler))
209→
210→ // REST API - Sessions
211→ .route("/api/sessions", get(list_sessions))
212→ .route("/api/sessions/:id", get(get_session))
213→ .route("/api/sessions/:id", delete(disconnect_session))
214→
215→<<<<<<< HEAD
216→ // REST API - Machines
217→ .route("/api/machines", get(list_machines))
218→ .route("/api/machines/:agent_id", get(get_machine))
219→ .route("/api/machines/:agent_id", delete(delete_machine))
220→ .route("/api/machines/:agent_id/history", get(get_machine_history))
221→
222→=======
223→>>>>>>> b861cb1 (Add user management system with JWT authentication)
224→ // HTML page routes (clean URLs)
225→ .route("/login", get(serve_login))
226→ .route("/dashboard", get(serve_dashboard))
227→ .route("/users", get(serve_users))
228→
229→ // State and middleware
230→ .with_state(state.clone())
231→ .layer(middleware::from_fn_with_state(state, auth_layer))
232→
233→ // Serve static files for portal (fallback)
234→ .fallback_service(ServeDir::new("static").append_index_html_on_directories(true))
235→
236→ // Middleware
237→ .layer(TraceLayer::new_for_http())
238→ .layer(
239→ CorsLayer::new()
240→ .allow_origin(Any)
241→ .allow_methods(Any)
242→ .allow_headers(Any),
243→ );
244→
245→ // Start server
246→ let addr: SocketAddr = listen_addr.parse()?;
247→ let listener = tokio::net::TcpListener::bind(addr).await?;
248→
249→ info!("Server listening on {}", addr);
250→
251→ axum::serve(listener, app).await?;
252→
253→ Ok(())
254→}
255→
256→async fn health() -> &'static str {
257→ "OK"
258→}
259→
260→// Support code API handlers
261→
262→async fn create_code(
263→ State(state): State<AppState>,
264→ Json(request): Json<CreateCodeRequest>,
265→) -> Json<SupportCode> {
266→ let code = state.support_codes.create_code(request).await;
267→ info!("Created support code: {}", code.code);
268→ Json(code)
269→}
270→
271→async fn list_codes(
272→ State(state): State<AppState>,
273→) -> Json<Vec<SupportCode>> {
274→ Json(state.support_codes.list_active_codes().await)
275→}
276→
277→#[derive(Deserialize)]
278→struct ValidateParams {
279→ code: String,
280→}
281→
282→async fn validate_code(
283→ State(state): State<AppState>,
284→ Path(code): Path<String>,
285→) -> Json<CodeValidation> {
286→ Json(state.support_codes.validate_code(&code).await)
287→}
288→
289→async fn cancel_code(
290→ State(state): State<AppState>,
291→ Path(code): Path<String>,
292→) -> impl IntoResponse {
293→ if state.support_codes.cancel_code(&code).await {
294→ (StatusCode::OK, "Code cancelled")
295→ } else {
296→ (StatusCode::BAD_REQUEST, "Cannot cancel code")
297→ }
298→}
299→
300→// Session API handlers (updated to use AppState)
301→
302→async fn list_sessions(
303→ State(state): State<AppState>,
304→) -> Json<Vec<api::SessionInfo>> {
305→ let sessions = state.sessions.list_sessions().await;
306→ Json(sessions.into_iter().map(api::SessionInfo::from).collect())
307→}
308→
309→async fn get_session(
310→ State(state): State<AppState>,
311→ Path(id): Path<String>,
312→) -> Result<Json<api::SessionInfo>, (StatusCode, &'static str)> {
313→ let session_id = uuid::Uuid::parse_str(&id)
314→ .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID"))?;
315→
316→ let session = state.sessions.get_session(session_id).await
317→ .ok_or((StatusCode::NOT_FOUND, "Session not found"))?;
318→
319→ Ok(Json(api::SessionInfo::from(session)))
320→}
321→
322→async fn disconnect_session(
323→ State(state): State<AppState>,
324→ Path(id): Path<String>,
325→) -> impl IntoResponse {
326→ let session_id = match uuid::Uuid::parse_str(&id) {
327→ Ok(id) => id,
328→ Err(_) => return (StatusCode::BAD_REQUEST, "Invalid session ID"),
329→ };
330→
331→ if state.sessions.disconnect_session(session_id, "Disconnected by administrator").await {
332→ info!("Session {} disconnected by admin", session_id);
333→ (StatusCode::OK, "Session disconnected")
334→ } else {
335→ (StatusCode::NOT_FOUND, "Session not found")
336→ }
337→}
338→
339→// Machine API handlers
340→
341→async fn list_machines(
342→ State(state): State<AppState>,
343→) -> Result<Json<Vec<api::MachineInfo>>, (StatusCode, &'static str)> {
344→ let db = state.db.as_ref()
345→ .ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
346→
347→ let machines = db::machines::get_all_machines(db.pool()).await
348→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
349→
350→ Ok(Json(machines.into_iter().map(api::MachineInfo::from).collect()))
351→}
352→
353→async fn get_machine(
354→ State(state): State<AppState>,
355→ Path(agent_id): Path<String>,
356→) -> Result<Json<api::MachineInfo>, (StatusCode, &'static str)> {
357→ let db = state.db.as_ref()
358→ .ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
359→
360→ let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
361→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
362→ .ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
363→
364→ Ok(Json(api::MachineInfo::from(machine)))
365→}
366→
367→async fn get_machine_history(
368→ State(state): State<AppState>,
369→ Path(agent_id): Path<String>,
370→) -> Result<Json<api::MachineHistory>, (StatusCode, &'static str)> {
371→ let db = state.db.as_ref()
372→ .ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
373→
374→ // Get machine
375→ let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
376→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
377→ .ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
378→
379→ // Get sessions for this machine
380→ let sessions = db::sessions::get_sessions_for_machine(db.pool(), machine.id).await
381→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
382→
383→ // Get events for this machine
384→ let events = db::events::get_events_for_machine(db.pool(), machine.id).await
385→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
386→
387→ let history = api::MachineHistory {
388→ machine: api::MachineInfo::from(machine),
389→ sessions: sessions.into_iter().map(api::SessionRecord::from).collect(),
390→ events: events.into_iter().map(api::EventRecord::from).collect(),
391→ exported_at: chrono::Utc::now().to_rfc3339(),
392→ };
393→
394→ Ok(Json(history))
395→}
396→
397→async fn delete_machine(
398→ State(state): State<AppState>,
399→ Path(agent_id): Path<String>,
400→ Query(params): Query<api::DeleteMachineParams>,
401→) -> Result<Json<api::DeleteMachineResponse>, (StatusCode, &'static str)> {
402→ let db = state.db.as_ref()
403→ .ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
404→
405→ // Get machine first
406→ let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
407→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
408→ .ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
409→
410→ // Export history if requested
411→ let history = if params.export {
412→ let sessions = db::sessions::get_sessions_for_machine(db.pool(), machine.id).await
413→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
414→ let events = db::events::get_events_for_machine(db.pool(), machine.id).await
415→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
416→
417→ Some(api::MachineHistory {
418→ machine: api::MachineInfo::from(machine.clone()),
419→ sessions: sessions.into_iter().map(api::SessionRecord::from).collect(),
420→ events: events.into_iter().map(api::EventRecord::from).collect(),
421→ exported_at: chrono::Utc::now().to_rfc3339(),
422→ })
423→ } else {
424→ None
425→ };
426→
427→ // Send uninstall command if requested and agent is online
428→ let mut uninstall_sent = false;
429→ if params.uninstall {
430→ // Find session for this agent
431→ if let Some(session) = state.sessions.get_session_by_agent(&agent_id).await {
432→ if session.is_online {
433→ uninstall_sent = state.sessions.send_admin_command(
434→ session.id,
435→ proto::AdminCommandType::AdminUninstall,
436→ "Deleted by administrator",
437→ ).await;
438→ if uninstall_sent {
439→ info!("Sent uninstall command to agent {}", agent_id);
440→ }
441→ }
442→ }
443→ }
444→
445→ // Remove from session manager
446→ state.sessions.remove_agent(&agent_id).await;
447→
448→ // Delete from database (cascades to sessions and events)
449→ db::machines::delete_machine(db.pool(), &agent_id).await
450→ .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to delete machine"))?;
451→
452→ info!("Deleted machine {} (uninstall_sent: {})", agent_id, uninstall_sent);
453→
454→ Ok(Json(api::DeleteMachineResponse {
455→ success: true,
456→ message: format!("Machine {} deleted", machine.hostname),
457→ uninstall_sent,
458→ history,
459→ }))
460→}
461→
462→// Static page handlers
463→async fn serve_login() -> impl IntoResponse {
464→ match tokio::fs::read_to_string("static/login.html").await {
465→ Ok(content) => Html(content).into_response(),
466→ Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
467→ }
468→}
469→
470→async fn serve_dashboard() -> impl IntoResponse {
471→ match tokio::fs::read_to_string("static/dashboard.html").await {
472→ Ok(content) => Html(content).into_response(),
473→ Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
474→ }
475→}
476→
477→async fn serve_users() -> impl IntoResponse {
478→ match tokio::fs::read_to_string("static/users.html").await {
479→ Ok(content) => Html(content).into_response(),
480→ Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(),
481→ }
482→}
483→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>