Provisiona RustDesk automaticamente
This commit is contained in:
parent
967d4bf1c6
commit
ef1db284fa
6 changed files with 565 additions and 9 deletions
|
|
@ -27,7 +27,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] }
|
sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] }
|
||||||
get_if_addrs = "0.5"
|
get_if_addrs = "0.5"
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
reqwest = { version = "0.12", features = ["json", "rustls-tls", "blocking"], default-features = false }
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
|
@ -35,3 +35,4 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
hostname = "0.4"
|
hostname = "0.4"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
mod agent;
|
mod agent;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod rustdesk;
|
||||||
|
|
||||||
use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile};
|
use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile};
|
||||||
use tauri_plugin_store::Builder as StorePluginBuilder;
|
use tauri_plugin_store::Builder as StorePluginBuilder;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RustdeskProvisioningResult {
|
||||||
|
pub id: String,
|
||||||
|
pub password: String,
|
||||||
|
pub installed_version: Option<String>,
|
||||||
|
pub updated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn collect_machine_profile() -> Result<MachineProfile, String> {
|
fn collect_machine_profile() -> Result<MachineProfile, String> {
|
||||||
collect_profile().map_err(|error| error.to_string())
|
collect_profile().map_err(|error| error.to_string())
|
||||||
|
|
@ -38,6 +49,26 @@ fn open_devtools(window: tauri::WebviewWindow) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn provision_rustdesk(machine_id: Option<String>) -> Result<RustdeskProvisioningResult, String> {
|
||||||
|
let machine_id = machine_id
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.ok_or_else(|| "Informe o identificador da máquina para provisionar o RustDesk.".to_string())?;
|
||||||
|
tauri::async_runtime::spawn_blocking(move || run_rustdesk_provision(machine_id))
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn run_rustdesk_provision(machine_id: String) -> Result<RustdeskProvisioningResult, String> {
|
||||||
|
rustdesk::provision(&machine_id).map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn run_rustdesk_provision(_machine_id: String) -> Result<RustdeskProvisioningResult, String> {
|
||||||
|
Err("Provisionamento automático do RustDesk está disponível apenas no Windows.".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
|
@ -51,7 +82,8 @@ pub fn run() {
|
||||||
collect_machine_inventory,
|
collect_machine_inventory,
|
||||||
start_machine_agent,
|
start_machine_agent,
|
||||||
stop_machine_agent,
|
stop_machine_agent,
|
||||||
open_devtools
|
open_devtools,
|
||||||
|
provision_rustdesk
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
|
||||||
270
apps/desktop/src-tauri/src/rustdesk.rs
Normal file
270
apps/desktop/src-tauri/src/rustdesk.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
#![cfg(target_os = "windows")]
|
||||||
|
|
||||||
|
use crate::RustdeskProvisioningResult;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::env;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const RELEASES_API: &str = "https://api.github.com/repos/rustdesk/rustdesk/releases/latest";
|
||||||
|
const USER_AGENT: &str = "RavenDesktop/1.0";
|
||||||
|
const SERVER_HOST: &str = "rust.rever.com.br";
|
||||||
|
const SERVER_KEY: &str = "0mxocQKmK6GvTZQYKgjrG9tlNkKOqf81gKgqwAmnZuI=";
|
||||||
|
const DEFAULT_PASSWORD: &str = "FMQ9MA>e73r.FI<b*34Vmx_8P";
|
||||||
|
const SERVICE_NAME: &str = "RustDesk";
|
||||||
|
const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache";
|
||||||
|
|
||||||
|
static PROVISION_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RustdeskError {
|
||||||
|
#[error("HTTP error: {0}")]
|
||||||
|
Http(#[from] reqwest::Error),
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
#[error("Release asset não encontrado para Windows x86_64")]
|
||||||
|
AssetMissing,
|
||||||
|
#[error("Falha ao executar comando {command}: status {status:?}")]
|
||||||
|
CommandFailed { command: String, status: Option<i32> },
|
||||||
|
#[error("Falha ao detectar ID do RustDesk")]
|
||||||
|
MissingId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ReleaseAsset {
|
||||||
|
name: String,
|
||||||
|
browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ReleaseResponse {
|
||||||
|
tag_name: String,
|
||||||
|
assets: Vec<ReleaseAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn provision(machine_id: &str) -> Result<RustdeskProvisioningResult, RustdeskError> {
|
||||||
|
let _guard = PROVISION_MUTEX.lock();
|
||||||
|
let exe_path = detect_executable_path();
|
||||||
|
let (installed_version, freshly_installed) = ensure_installed(&exe_path)?;
|
||||||
|
let config_path = write_config_files()?;
|
||||||
|
apply_config(&exe_path, &config_path)?;
|
||||||
|
set_password(&exe_path)?;
|
||||||
|
let custom_id = set_custom_id(&exe_path, machine_id)?;
|
||||||
|
ensure_service_running()?;
|
||||||
|
let reported_id = query_id(&exe_path).unwrap_or(custom_id.clone());
|
||||||
|
let version = query_version(&exe_path).ok().or(installed_version);
|
||||||
|
|
||||||
|
Ok(RustdeskProvisioningResult {
|
||||||
|
id: reported_id,
|
||||||
|
password: DEFAULT_PASSWORD.to_string(),
|
||||||
|
installed_version: version,
|
||||||
|
updated: freshly_installed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_executable_path() -> PathBuf {
|
||||||
|
let program_files = env::var("PROGRAMFILES").unwrap_or_else(|_| "C:/Program Files".to_string());
|
||||||
|
Path::new(&program_files).join("RustDesk").join("rustdesk.exe")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_installed(exe_path: &Path) -> Result<(Option<String>, bool), RustdeskError> {
|
||||||
|
if exe_path.exists() {
|
||||||
|
return Ok((None, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache_root = PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string()))
|
||||||
|
.join(CACHE_DIR_NAME);
|
||||||
|
fs::create_dir_all(&cache_root)?;
|
||||||
|
|
||||||
|
let (installer_path, version_tag) = download_latest_installer(&cache_root)?;
|
||||||
|
run_installer(&installer_path)?;
|
||||||
|
thread::sleep(Duration::from_secs(20));
|
||||||
|
|
||||||
|
Ok((Some(version_tag), true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_latest_installer(cache_root: &Path) -> Result<(PathBuf, String), RustdeskError> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.user_agent(USER_AGENT)
|
||||||
|
.timeout(Duration::from_secs(60))
|
||||||
|
.build()?;
|
||||||
|
let release: ReleaseResponse = client.get(RELEASES_API).send()?.error_for_status()?.json()?;
|
||||||
|
let asset = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.name.ends_with("x86_64.exe"))
|
||||||
|
.ok_or(RustdeskError::AssetMissing)?;
|
||||||
|
let target_path = cache_root.join(&asset.name);
|
||||||
|
if target_path.exists() {
|
||||||
|
return Ok((target_path, release.tag_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response = client.get(&asset.browser_download_url).send()?.error_for_status()?;
|
||||||
|
let mut output = File::create(&target_path)?;
|
||||||
|
response.copy_to(&mut output)?;
|
||||||
|
Ok((target_path, release.tag_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_installer(installer_path: &Path) -> Result<(), RustdeskError> {
|
||||||
|
let status = Command::new(installer_path)
|
||||||
|
.arg("--silent-install")
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(RustdeskError::CommandFailed {
|
||||||
|
command: format!("{} --silent-install", installer_path.display()),
|
||||||
|
status: status.code(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_config_files() -> Result<PathBuf, RustdeskError> {
|
||||||
|
let config_contents = build_config_contents();
|
||||||
|
let program_data = PathBuf::from(env::var("PROGRAMDATA").unwrap_or_else(|_| "C:/ProgramData".to_string()));
|
||||||
|
let main_path = program_data.join("RustDesk").join("config").join("RustDesk2.toml");
|
||||||
|
write_file(&main_path, &config_contents)?;
|
||||||
|
|
||||||
|
let service_profile = PathBuf::from(r"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config\\RustDesk2.toml");
|
||||||
|
write_file(&service_profile, &config_contents).ok();
|
||||||
|
|
||||||
|
Ok(main_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_file(path: &Path, contents: &str) -> Result<(), io::Error> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
fs::write(path, contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_config_contents() -> String {
|
||||||
|
format!(
|
||||||
|
r#"rendezvous_server = "{host}"
|
||||||
|
relay_server = "{host}"
|
||||||
|
api_server = ""
|
||||||
|
serial = 0
|
||||||
|
|
||||||
|
[options]
|
||||||
|
key = "{key}"
|
||||||
|
relay-server = "{host}"
|
||||||
|
custom-rendezvous-server = "{host}"
|
||||||
|
"#,
|
||||||
|
host = SERVER_HOST,
|
||||||
|
key = SERVER_KEY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_config(exe_path: &Path, config_path: &Path) -> Result<(), RustdeskError> {
|
||||||
|
let status = Command::new(exe_path)
|
||||||
|
.arg("--import-config")
|
||||||
|
.arg(config_path)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(RustdeskError::CommandFailed {
|
||||||
|
command: format!("{} --import-config {}", exe_path.display(), config_path.display()),
|
||||||
|
status: status.code(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_password(exe_path: &Path) -> Result<(), RustdeskError> {
|
||||||
|
run_with_args(exe_path, &["--password", DEFAULT_PASSWORD])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_custom_id(exe_path: &Path, machine_id: &str) -> Result<String, RustdeskError> {
|
||||||
|
let custom_id = derive_numeric_id(machine_id);
|
||||||
|
run_with_args(exe_path, &["--set-id", &custom_id])?;
|
||||||
|
Ok(custom_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_numeric_id(machine_id: &str) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(machine_id.as_bytes());
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
let mut bytes = [0u8; 8];
|
||||||
|
bytes.copy_from_slice(&hash[..8]);
|
||||||
|
let value = u64::from_le_bytes(bytes);
|
||||||
|
let num = (value % 900_000_000) + 100_000_000;
|
||||||
|
format!("{:09}", num)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_service_running() -> Result<(), RustdeskError> {
|
||||||
|
let _ = run_sc(&["stop", SERVICE_NAME]);
|
||||||
|
thread::sleep(Duration::from_secs(2));
|
||||||
|
let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]);
|
||||||
|
run_sc(&["start", SERVICE_NAME])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_sc(args: &[&str]) -> Result<(), RustdeskError> {
|
||||||
|
let status = Command::new("sc")
|
||||||
|
.args(args)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(RustdeskError::CommandFailed {
|
||||||
|
command: format!("sc {}", args.join(" ")),
|
||||||
|
status: status.code(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_id(exe_path: &Path) -> Result<String, RustdeskError> {
|
||||||
|
let output = Command::new(exe_path)
|
||||||
|
.arg("--get-id")
|
||||||
|
.output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(RustdeskError::CommandFailed {
|
||||||
|
command: format!("{} --get-id", exe_path.display()),
|
||||||
|
status: output.status.code(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if stdout.is_empty() {
|
||||||
|
return Err(RustdeskError::MissingId);
|
||||||
|
}
|
||||||
|
Ok(stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_version(exe_path: &Path) -> Result<String, RustdeskError> {
|
||||||
|
let output = Command::new(exe_path)
|
||||||
|
.arg("--version")
|
||||||
|
.output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(RustdeskError::CommandFailed {
|
||||||
|
command: format!("{} --version", exe_path.display()),
|
||||||
|
status: output.status.code(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> {
|
||||||
|
let status = Command::new(exe_path)
|
||||||
|
.args(args)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(RustdeskError::CommandFailed {
|
||||||
|
command: format!("{} {}", exe_path.display(), args.join(" ")),
|
||||||
|
status: status.code(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -76,6 +76,20 @@ type AgentConfig = {
|
||||||
heartbeatIntervalSec?: number | null
|
heartbeatIntervalSec?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RustdeskProvisioningResult = {
|
||||||
|
id: string
|
||||||
|
password: string
|
||||||
|
installedVersion?: string | null
|
||||||
|
updated: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type RustdeskInfo = RustdeskProvisioningResult & {
|
||||||
|
lastProvisionedAt: number
|
||||||
|
lastSyncedAt?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const RUSTDESK_STORE_KEY = "rustdesk"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APP_URL?: string
|
readonly VITE_APP_URL?: string
|
||||||
|
|
@ -128,6 +142,15 @@ async function writeConfig(store: Store, cfg: AgentConfig): Promise<void> {
|
||||||
await store.save()
|
await store.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readRustdeskInfo(store: Store): Promise<RustdeskInfo | null> {
|
||||||
|
return (await store.get<RustdeskInfo>(RUSTDESK_STORE_KEY)) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeRustdeskInfo(store: Store, info: RustdeskInfo): Promise<void> {
|
||||||
|
await store.set(RUSTDESK_STORE_KEY, info)
|
||||||
|
await store.save()
|
||||||
|
}
|
||||||
|
|
||||||
function bytes(n?: number) {
|
function bytes(n?: number) {
|
||||||
if (!n || !Number.isFinite(n)) return "—"
|
if (!n || !Number.isFinite(n)) return "—"
|
||||||
const u = ["B","KB","MB","GB","TB"]
|
const u = ["B","KB","MB","GB","TB"]
|
||||||
|
|
@ -191,6 +214,9 @@ function App() {
|
||||||
const [tokenValidationTick, setTokenValidationTick] = useState(0)
|
const [tokenValidationTick, setTokenValidationTick] = useState(0)
|
||||||
const [, setIsValidatingToken] = useState(false)
|
const [, setIsValidatingToken] = useState(false)
|
||||||
const tokenVerifiedRef = useRef(false)
|
const tokenVerifiedRef = useRef(false)
|
||||||
|
const [rustdeskInfo, setRustdeskInfo] = useState<RustdeskInfo | null>(null)
|
||||||
|
const [isRustdeskProvisioning, setIsRustdeskProvisioning] = useState(false)
|
||||||
|
const rustdeskBootstrapRef = useRef(false)
|
||||||
|
|
||||||
const [provisioningCode, setProvisioningCode] = useState("")
|
const [provisioningCode, setProvisioningCode] = useState("")
|
||||||
const [validatedCompany, setValidatedCompany] = useState<{ id: string; name: string; slug: string; tenantId: string } | null>(null)
|
const [validatedCompany, setValidatedCompany] = useState<{ id: string; name: string; slug: string; tenantId: string } | null>(null)
|
||||||
|
|
@ -219,6 +245,8 @@ function App() {
|
||||||
setToken(t)
|
setToken(t)
|
||||||
const cfg = await readConfig(s)
|
const cfg = await readConfig(s)
|
||||||
setConfig(cfg)
|
setConfig(cfg)
|
||||||
|
const existingRustdesk = await readRustdeskInfo(s)
|
||||||
|
setRustdeskInfo(existingRustdesk)
|
||||||
if (cfg?.collaboratorEmail) setCollabEmail(cfg.collaboratorEmail)
|
if (cfg?.collaboratorEmail) setCollabEmail(cfg.collaboratorEmail)
|
||||||
if (cfg?.collaboratorName) setCollabName(cfg.collaboratorName)
|
if (cfg?.collaboratorName) setCollabName(cfg.collaboratorName)
|
||||||
if (cfg?.companyName) setCompanyName(cfg.companyName)
|
if (cfg?.companyName) setCompanyName(cfg.companyName)
|
||||||
|
|
@ -426,14 +454,94 @@ function App() {
|
||||||
}
|
}
|
||||||
}, [provisioningCode])
|
}, [provisioningCode])
|
||||||
|
|
||||||
const resolvedAppUrl = useMemo(() => {
|
const resolvedAppUrl = useMemo(() => {
|
||||||
if (!config?.appUrl) return appUrl
|
if (!config?.appUrl) return appUrl
|
||||||
const normalized = normalizeUrl(config.appUrl, appUrl)
|
const normalized = normalizeUrl(config.appUrl, appUrl)
|
||||||
if (import.meta.env.MODE === "production" && normalized.includes("localhost")) {
|
if (import.meta.env.MODE === "production" && normalized.includes("localhost")) {
|
||||||
return appUrl
|
return appUrl
|
||||||
}
|
}
|
||||||
return normalized
|
return normalized
|
||||||
}, [config?.appUrl])
|
}, [config?.appUrl])
|
||||||
|
|
||||||
|
const syncRustdeskAccess = useCallback(
|
||||||
|
async (machineToken: string, info: RustdeskInfo) => {
|
||||||
|
if (!store || !machineToken) return
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/api/machines/remote-access`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
machineToken,
|
||||||
|
provider: "RustDesk",
|
||||||
|
identifier: info.id,
|
||||||
|
url: `rustdesk://${info.id}`,
|
||||||
|
password: info.password,
|
||||||
|
notes: info.installedVersion ? `RustDesk ${info.installedVersion}` : undefined,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text()
|
||||||
|
throw new Error(text.slice(0, 300) || "Falha ao registrar acesso remoto")
|
||||||
|
}
|
||||||
|
const nextInfo: RustdeskInfo = { ...info, lastSyncedAt: Date.now() }
|
||||||
|
await writeRustdeskInfo(store, nextInfo)
|
||||||
|
setRustdeskInfo(nextInfo)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Falha ao sincronizar acesso remoto com a plataforma", error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[store]
|
||||||
|
)
|
||||||
|
|
||||||
|
const provisionRustdesk = useCallback(
|
||||||
|
async (machineId: string, machineToken: string): Promise<RustdeskInfo | null> => {
|
||||||
|
if (!store || !machineId) return null
|
||||||
|
setIsRustdeskProvisioning(true)
|
||||||
|
try {
|
||||||
|
const result = await invoke<RustdeskProvisioningResult>("provision_rustdesk", { machineId })
|
||||||
|
const info: RustdeskInfo = {
|
||||||
|
...result,
|
||||||
|
lastProvisionedAt: Date.now(),
|
||||||
|
lastSyncedAt: null,
|
||||||
|
}
|
||||||
|
await writeRustdeskInfo(store, info)
|
||||||
|
setRustdeskInfo(info)
|
||||||
|
if (machineToken) {
|
||||||
|
await syncRustdeskAccess(machineToken, info)
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
if (message.toLowerCase().includes("apenas no windows")) {
|
||||||
|
console.info("Provisionamento do RustDesk ignorado (plataforma não suportada)")
|
||||||
|
} else {
|
||||||
|
console.error("Falha ao provisionar RustDesk", error)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
setIsRustdeskProvisioning(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[store, syncRustdeskAccess]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!store || !config?.machineId || !token) return
|
||||||
|
if (!rustdeskInfo && !isRustdeskProvisioning && !rustdeskBootstrapRef.current) {
|
||||||
|
rustdeskBootstrapRef.current = true
|
||||||
|
provisionRustdesk(config.machineId, token).finally(() => {
|
||||||
|
rustdeskBootstrapRef.current = false
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rustdeskInfo && !isRustdeskProvisioning) {
|
||||||
|
const lastSync = rustdeskInfo.lastSyncedAt ?? 0
|
||||||
|
const needsSync = Date.now() - lastSync > 7 * 24 * 60 * 60 * 1000
|
||||||
|
if (needsSync) {
|
||||||
|
syncRustdeskAccess(token, rustdeskInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [store, config?.machineId, token, rustdeskInfo, provisionRustdesk, syncRustdeskAccess, isRustdeskProvisioning])
|
||||||
|
|
||||||
async function register() {
|
async function register() {
|
||||||
if (!profile) return
|
if (!profile) return
|
||||||
|
|
@ -524,6 +632,8 @@ function App() {
|
||||||
setToken(data.machineToken)
|
setToken(data.machineToken)
|
||||||
setCompanyName(validatedCompany.name)
|
setCompanyName(validatedCompany.name)
|
||||||
|
|
||||||
|
await provisionRustdesk(data.machineId, data.machineToken)
|
||||||
|
|
||||||
await invoke("start_machine_agent", {
|
await invoke("start_machine_agent", {
|
||||||
baseUrl: apiBaseUrl,
|
baseUrl: apiBaseUrl,
|
||||||
token: data.machineToken,
|
token: data.machineToken,
|
||||||
|
|
@ -885,6 +995,9 @@ function App() {
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-2 flex gap-2">
|
<div className="mt-2 flex gap-2">
|
||||||
<button disabled={busy || !validatedCompany || !isEmailValid || !collabName.trim() || provisioningCode.trim().length < 32} onClick={register} className="rounded-lg border border-black bg-black px-3 py-2 text-sm font-semibold text-white hover:bg-black/90 disabled:opacity-60">Registrar dispositivo</button>
|
<button disabled={busy || !validatedCompany || !isEmailValid || !collabName.trim() || provisioningCode.trim().length < 32} onClick={register} className="rounded-lg border border-black bg-black px-3 py-2 text-sm font-semibold text-white hover:bg-black/90 disabled:opacity-60">Registrar dispositivo</button>
|
||||||
|
{isRustdeskProvisioning ? (
|
||||||
|
<p className="text-xs text-neutral-500">Preparando cliente de acesso remoto (RustDesk)...</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -2261,6 +2261,87 @@ export const updateRemoteAccess = mutation({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const upsertRemoteAccessViaToken = mutation({
|
||||||
|
args: {
|
||||||
|
machineToken: v.string(),
|
||||||
|
provider: v.string(),
|
||||||
|
identifier: v.string(),
|
||||||
|
url: v.optional(v.string()),
|
||||||
|
username: v.optional(v.string()),
|
||||||
|
password: v.optional(v.string()),
|
||||||
|
notes: v.optional(v.string()),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const { machine } = await getActiveToken(ctx, args.machineToken)
|
||||||
|
const trimmedProvider = args.provider.trim()
|
||||||
|
const trimmedIdentifier = args.identifier.trim()
|
||||||
|
if (!trimmedProvider || !trimmedIdentifier) {
|
||||||
|
throw new ConvexError("Informe provedor e identificador do acesso remoto.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalizedUrl: string | null = null
|
||||||
|
if (args.url) {
|
||||||
|
const trimmedUrl = args.url.trim()
|
||||||
|
if (trimmedUrl) {
|
||||||
|
const isValidScheme = /^https?:\/\//i.test(trimmedUrl) || /^rustdesk:\/\//i.test(trimmedUrl)
|
||||||
|
if (!isValidScheme) {
|
||||||
|
throw new ConvexError("Informe uma URL iniciando com http://, https:// ou rustdesk://.")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new URL(trimmedUrl.replace(/^rustdesk:\/\//i, "https://"))
|
||||||
|
} catch {
|
||||||
|
throw new ConvexError("Informe uma URL válida para o acesso remoto.")
|
||||||
|
}
|
||||||
|
normalizedUrl = trimmedUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedUsername = args.username?.trim() ? args.username.trim() : null
|
||||||
|
const cleanedPassword = args.password?.trim() ? args.password.trim() : null
|
||||||
|
const cleanedNotes = args.notes?.trim() ? args.notes.trim() : null
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const existingEntries = normalizeRemoteAccessList(machine.remoteAccess)
|
||||||
|
const existingIndex = existingEntries.findIndex(
|
||||||
|
(entry) =>
|
||||||
|
entry.provider.toLowerCase() === trimmedProvider.toLowerCase() &&
|
||||||
|
entry.identifier.toLowerCase() === trimmedIdentifier.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
|
const entryId = existingIndex >= 0 ? existingEntries[existingIndex].id : createRemoteAccessId()
|
||||||
|
const updatedEntry: RemoteAccessEntry = {
|
||||||
|
id: entryId,
|
||||||
|
provider: trimmedProvider,
|
||||||
|
identifier: trimmedIdentifier,
|
||||||
|
url: normalizedUrl,
|
||||||
|
username: cleanedUsername,
|
||||||
|
password: cleanedPassword,
|
||||||
|
notes: cleanedNotes,
|
||||||
|
lastVerifiedAt: timestamp,
|
||||||
|
metadata: {
|
||||||
|
provider: trimmedProvider,
|
||||||
|
identifier: trimmedIdentifier,
|
||||||
|
url: normalizedUrl,
|
||||||
|
username: cleanedUsername,
|
||||||
|
password: cleanedPassword,
|
||||||
|
notes: cleanedNotes,
|
||||||
|
lastVerifiedAt: timestamp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextEntries =
|
||||||
|
existingIndex >= 0
|
||||||
|
? existingEntries.map((entry, index) => (index === existingIndex ? updatedEntry : entry))
|
||||||
|
: [...existingEntries, updatedEntry]
|
||||||
|
|
||||||
|
await ctx.db.patch(machine._id, {
|
||||||
|
remoteAccess: nextEntries,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { remoteAccess: nextEntries }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const remove = mutation({
|
export const remove = mutation({
|
||||||
args: {
|
args: {
|
||||||
machineId: v.id("machines"),
|
machineId: v.id("machines"),
|
||||||
|
|
|
||||||
59
src/app/api/machines/remote-access/route.ts
Normal file
59
src/app/api/machines/remote-access/route.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { api } from "@/convex/_generated/api"
|
||||||
|
import { createCorsPreflight, jsonWithCors } from "@/server/cors"
|
||||||
|
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
machineToken: z.string().min(1),
|
||||||
|
provider: z.string().min(1),
|
||||||
|
identifier: z.string().min(1),
|
||||||
|
url: z.string().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
password: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const METHODS = "POST, OPTIONS"
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return createCorsPreflight(request.headers.get("origin"), METHODS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const origin = request.headers.get("origin")
|
||||||
|
if (request.method !== "POST") {
|
||||||
|
return jsonWithCors({ error: "Método não permitido" }, 405, origin, METHODS)
|
||||||
|
}
|
||||||
|
|
||||||
|
let client
|
||||||
|
try {
|
||||||
|
client = createConvexClient()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ConvexConfigurationError) {
|
||||||
|
return jsonWithCors({ error: error.message }, 500, origin, METHODS)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload
|
||||||
|
try {
|
||||||
|
payload = schema.parse(await request.json())
|
||||||
|
} catch (error) {
|
||||||
|
return jsonWithCors(
|
||||||
|
{ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) },
|
||||||
|
400,
|
||||||
|
origin,
|
||||||
|
METHODS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.mutation(api.devices.upsertRemoteAccessViaToken, payload)
|
||||||
|
return jsonWithCors({ ok: true, remoteAccess: response?.remoteAccess ?? null }, 200, origin, METHODS)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[machines.remote-access:token] Falha ao registrar acesso remoto", error)
|
||||||
|
const details = error instanceof Error ? error.message : String(error)
|
||||||
|
return jsonWithCors({ error: "Falha ao registrar acesso remoto", details }, 500, origin, METHODS)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue