Add support codes API and portal server changes

- support_codes.rs: 6-digit code management
- main.rs: Portal routes, static file serving, AppState
- relay/mod.rs: Updated for AppState
- Cargo.toml: Added rand, tower-http fs feature

Generated with Claude Code
This commit is contained in:
2025-12-28 17:54:05 +00:00
parent 70a9fcd129
commit 611bc00d06
5 changed files with 347 additions and 17 deletions

199
server/src/support_codes.rs Normal file
View File

@@ -0,0 +1,199 @@
//! Support session codes management
//!
//! Handles generation and validation of 6-digit support codes
//! for one-time remote support sessions.
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use chrono::{DateTime, Utc};
use rand::Rng;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// A support session code
#[derive(Debug, Clone, Serialize)]
pub struct SupportCode {
pub code: String,
pub session_id: Uuid,
pub created_by: String,
pub created_at: DateTime<Utc>,
pub status: CodeStatus,
pub client_name: Option<String>,
pub client_machine: Option<String>,
pub connected_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CodeStatus {
Pending, // Waiting for client to connect
Connected, // Client connected, session active
Completed, // Session ended normally
Cancelled, // Code cancelled by tech
}
/// Request to create a new support code
#[derive(Debug, Deserialize)]
pub struct CreateCodeRequest {
pub technician_id: Option<String>,
pub technician_name: Option<String>,
}
/// Response when a code is validated
#[derive(Debug, Serialize)]
pub struct CodeValidation {
pub valid: bool,
pub session_id: Option<String>,
pub server_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
/// Manages support codes
#[derive(Clone)]
pub struct SupportCodeManager {
codes: Arc<RwLock<HashMap<String, SupportCode>>>,
session_to_code: Arc<RwLock<HashMap<Uuid, String>>>,
}
impl SupportCodeManager {
pub fn new() -> Self {
Self {
codes: Arc::new(RwLock::new(HashMap::new())),
session_to_code: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Generate a unique 6-digit code
async fn generate_unique_code(&self) -> String {
let codes = self.codes.read().await;
let mut rng = rand::thread_rng();
loop {
let code: u32 = rng.gen_range(100000..999999);
let code_str = code.to_string();
if !codes.contains_key(&code_str) {
return code_str;
}
}
}
/// Create a new support code
pub async fn create_code(&self, request: CreateCodeRequest) -> SupportCode {
let code = self.generate_unique_code().await;
let session_id = Uuid::new_v4();
let support_code = SupportCode {
code: code.clone(),
session_id,
created_by: request.technician_name.unwrap_or_else(|| "Unknown".to_string()),
created_at: Utc::now(),
status: CodeStatus::Pending,
client_name: None,
client_machine: None,
connected_at: None,
};
let mut codes = self.codes.write().await;
codes.insert(code.clone(), support_code.clone());
let mut session_to_code = self.session_to_code.write().await;
session_to_code.insert(session_id, code);
support_code
}
/// Validate a code and return session info
pub async fn validate_code(&self, code: &str) -> CodeValidation {
let codes = self.codes.read().await;
match codes.get(code) {
Some(support_code) => {
if support_code.status == CodeStatus::Pending || support_code.status == CodeStatus::Connected {
CodeValidation {
valid: true,
session_id: Some(support_code.session_id.to_string()),
server_url: Some("wss://connect.azcomputerguru.com/ws/support".to_string()),
error: None,
}
} else {
CodeValidation {
valid: false,
session_id: None,
server_url: None,
error: Some("This code has expired or been used".to_string()),
}
}
}
None => CodeValidation {
valid: false,
session_id: None,
server_url: None,
error: Some("Invalid code".to_string()),
},
}
}
/// Mark a code as connected
pub async fn mark_connected(&self, code: &str, client_name: Option<String>, client_machine: Option<String>) {
let mut codes = self.codes.write().await;
if let Some(support_code) = codes.get_mut(code) {
support_code.status = CodeStatus::Connected;
support_code.client_name = client_name;
support_code.client_machine = client_machine;
support_code.connected_at = Some(Utc::now());
}
}
/// Mark a code as completed
pub async fn mark_completed(&self, code: &str) {
let mut codes = self.codes.write().await;
if let Some(support_code) = codes.get_mut(code) {
support_code.status = CodeStatus::Completed;
}
}
/// Cancel a code
pub async fn cancel_code(&self, code: &str) -> bool {
let mut codes = self.codes.write().await;
if let Some(support_code) = codes.get_mut(code) {
if support_code.status == CodeStatus::Pending {
support_code.status = CodeStatus::Cancelled;
return true;
}
}
false
}
/// List all codes (for dashboard)
pub async fn list_codes(&self) -> Vec<SupportCode> {
let codes = self.codes.read().await;
codes.values().cloned().collect()
}
/// List active codes only
pub async fn list_active_codes(&self) -> Vec<SupportCode> {
let codes = self.codes.read().await;
codes.values()
.filter(|c| c.status == CodeStatus::Pending || c.status == CodeStatus::Connected)
.cloned()
.collect()
}
/// Get code by session ID
pub async fn get_by_session(&self, session_id: Uuid) -> Option<SupportCode> {
let session_to_code = self.session_to_code.read().await;
let code = session_to_code.get(&session_id)?;
let codes = self.codes.read().await;
codes.get(code).cloned()
}
}
impl Default for SupportCodeManager {
fn default() -> Self {
Self::new()
}
}