//! ReBreak Magic für Windows — Tauri-Core. //! //! Alle Backend-Calls + Token-Handling laufen hier in Rust; die WebView //! sieht nur den aggregierten UI-State (nie Tokens). Analog zur Mac-App //! (rebreak-magic-mac), gleiche /api/magic/*-Endpoints. mod api; mod auth; mod device; mod setup; use api::Api; use protection_core::ProtectionState; use serde::Serialize; use tauri::Manager; const DEFAULT_BACKEND_URL: &str = "https://staging.rebreak.org"; #[derive(Serialize, Clone, Default)] #[serde(rename_all = "camelCase")] struct UiState { backend_url: String, logged_in: bool, nickname: Option, plan: Option, device_id: String, hostname: String, /// Magic-Binding existiert (DNS-Token provisioniert) registered: bool, /// Windows-DoH zeigt aktuell auf ReBreak (lokaler Check) protection_applied: bool, release_requested_at: Option, release_available_at: Option, } /// Backend-URL: config.json im App-Config-Dir > Default (Staging). /// Gleiche Semantik wie config.example.json der Mac-App. fn backend_url(app: &tauri::AppHandle) -> String { if let Ok(dir) = app.path().app_config_dir() { if let Ok(raw) = std::fs::read_to_string(dir.join("config.json")) { if let Ok(v) = serde_json::from_str::(&raw) { if let Some(url) = v.get("backendBaseUrl").and_then(|u| u.as_str()) { return url.trim_end_matches('/').to_string(); } } } } DEFAULT_BACKEND_URL.into() } /// Aggregiert den kompletten UI-State (ein Roundtrip fürs Frontend). async fn build_state(app: &tauri::AppHandle) -> UiState { let mut state = UiState { backend_url: backend_url(app), device_id: device::device_id(), hostname: device::hostname(), ..Default::default() }; // Lokaler DoH-Check ist auth-unabhängig let server_ip = protection_core::load_state() .map(|s| s.server_ip) .unwrap_or_else(setup::resolve_doh_ip); state.protection_applied = protection_core::doh_is_applied(&server_ip); let Some(token) = auth::load_session_token() else { return state; }; let api = Api::new(&state.backend_url); match api.me(&token).await { Ok(me) => { state.logged_in = true; state.nickname = me.nickname; state.plan = me.plan; } Err(_) => { // Token abgelaufen/revoked → wie ausgeloggt behandeln return state; } } if let Ok(devices) = api.devices(&token).await { if let Some(mine) = devices.iter().find(|d| d.device_id == state.device_id) { state.registered = true; state.release_requested_at = mine.release_requested_at.clone(); state.release_available_at = mine.release_available_at.clone(); } } state } // --------------------------------------------------------------------------- // Tauri-Commands — jedes gibt den frischen UiState zurück // --------------------------------------------------------------------------- #[tauri::command] async fn get_state(app: tauri::AppHandle) -> Result { Ok(build_state(&app).await) } /// Pairing-Code einlösen + Device registrieren (ein Schritt fürs UI). #[tauri::command] async fn pair_and_register(app: tauri::AppHandle, code: String) -> Result { let code = code.trim().to_string(); if !code.chars().all(|c| c.is_ascii_digit()) || code.len() != 6 { return Err("Der Code besteht aus 6 Ziffern.".into()); } let base = backend_url(&app); let api = Api::new(&base); let hostname = device::hostname(); let pair = api.redeem_pair(&code, &hostname).await?; auth::save_session_token(&pair.token)?; let reg = api .register( &pair.token, &device::device_id(), &hostname, "Windows-PC", &device::os_version(), ) .await?; auth::save_dns_token(®.dns_token)?; Ok(build_state(&app).await) } /// Schutz aktivieren: EIN UAC-Prompt → DoH + State + Tamper-Service. #[tauri::command] async fn activate_protection(app: tauri::AppHandle) -> Result { let dns_token = auth::load_dns_token() .ok_or_else(|| "Kein DNS-Token — bitte zuerst koppeln.".to_string())?; let server_ip = setup::resolve_doh_ip(); let state = ProtectionState { doh_template: ProtectionState::doh_template_for(&dns_token), dns_token, server_ip, backend_url: backend_url(&app), device_id: device::device_id(), }; let service_exe = setup::service_exe_path()?; let script = setup::build_setup_script(&state, &service_exe.display().to_string()); setup::run_elevated(&script)?; let ui = build_state(&app).await; if !ui.protection_applied { return Err("Setup lief durch, aber der DoH-Schutz ist nicht aktiv. Bitte erneut versuchen.".into()); } Ok(ui) } /// 24h-Cooldown starten. Teardown macht NICHT die App — der SYSTEM-Service /// pollt /api/magic/status und baut erst ab wenn das Backend revoked. #[tauri::command] async fn request_release(app: tauri::AppHandle) -> Result { let token = auth::load_session_token().ok_or("Nicht eingeloggt")?; let api = Api::new(&backend_url(&app)); api.request_release(&token, &device::device_id()).await?; Ok(build_state(&app).await) } #[tauri::command] async fn cancel_release(app: tauri::AppHandle) -> Result { let token = auth::load_session_token().ok_or("Nicht eingeloggt")?; let api = Api::new(&backend_url(&app)); api.cancel_release(&token, &device::device_id()).await?; Ok(build_state(&app).await) } /// Logout entfernt nur die Session — Schutz + Service bleiben aktiv /// (Deaktivierung geht ausschließlich über den 24h-Release-Flow). #[tauri::command] async fn logout(app: tauri::AppHandle) -> Result { auth::clear_all(); Ok(build_state(&app).await) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ get_state, pair_and_register, activate_protection, request_release, cancel_release, logout, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }