mod admin; mod auth; mod storage; mod profile; mod cinema; use admin::DeviceSecurityInfo; use auth::{AuthResponse, DbStatus, ResetRequestResponse}; use serde::{Deserialize, Serialize}; use storage::{ServerUserDto, StorageItemDto, StorageStats}; #[derive(Debug, Serialize)] pub struct SafeDbInfo { pub host: String, pub port: u16, pub user: String, pub dbname: String, pub ssl_mode: String, pub users_table: String, pub email_column: String, } #[tauri::command] async fn check_db() -> Result { Ok(auth::check_db_health().await) } #[tauri::command] async fn login(email: String, password: String) -> Result { Ok(auth::authenticate_user(&email, &password).await) } #[tauri::command] async fn register(email: String, password: String, username: Option) -> Result { Ok(auth::register_user(&email, &password, username.as_deref()).await) } #[tauri::command] async fn request_registration_code(email: String, username: Option) -> Result { Ok(auth::request_registration_code(&email, username.as_deref()).await) } #[tauri::command] async fn register_with_code( email: String, code: String, password: String, username: Option, ) -> Result { Ok(auth::register_with_code(&email, &code, &password, username.as_deref()).await) } #[tauri::command] async fn validate_email(email: String) -> Result { let (_, domain) = auth::validate_email_syntax(&email)?; auth::verify_domain_exists(domain).await?; auth::verify_mailbox_exists(&email).await?; if let Ok(is_taken) = auth::check_email_already_registered(&email).await && is_taken { return Err("На этот адрес электронной почты уже зарегистрирован аккаунт. На одну почту можно создать только 1 аккаунт.".to_string()); } Ok(true) } #[tauri::command] async fn request_reset_code(email: String) -> Result { Ok(auth::request_reset_code(&email).await) } #[tauri::command] fn verify_reset_code(email: String, code: String) -> Result { Ok(auth::verify_reset_code(&email, &code)) } #[tauri::command] async fn reset_password(email: String, code: String, new_password: String) -> Result { Ok(auth::reset_password_with_code(&email, &code, &new_password).await) } #[tauri::command] fn get_db_info() -> SafeDbInfo { SafeDbInfo { host: "Защищенный сервер (Home Autli)".to_string(), port: 0, user: "protected".to_string(), dbname: "protected".to_string(), ssl_mode: "tls".to_string(), users_table: "accounts".to_string(), email_column: "email".to_string(), } } // Storage commands #[tauri::command] async fn get_storage_stats() -> Result { storage::get_server_storage_stats().await } #[tauri::command] async fn list_storage_items(user_email: String, folder_id: Option) -> Result, String> { storage::list_server_items(&user_email, folder_id).await } #[tauri::command] async fn create_server_folder(user_email: String, folder_name: String, parent_id: Option) -> Result { storage::create_server_folder(&user_email, &folder_name, parent_id).await } #[tauri::command] async fn delete_server_item(user_email: String, item_id: String) -> Result { storage::delete_server_item(&user_email, &item_id).await } #[tauri::command] async fn rename_server_item(user_email: String, item_id: String, new_name: String) -> Result { storage::rename_server_item(&user_email, &item_id, &new_name).await } #[tauri::command] async fn create_server_file( user_email: String, file_name: String, content: Option, folder_id: Option, ) -> Result { storage::create_server_file(&user_email, &file_name, content, folder_id).await } #[tauri::command] async fn save_server_file_content( user_email: String, file_id: i64, content: String, ) -> Result { storage::save_server_file_content(&user_email, file_id, &content).await } #[tauri::command] async fn upload_server_file( user_email: String, file_name: String, size_bytes: i64, mime_type: Option, content_base64: Option, folder_id: Option, ) -> Result { storage::upload_server_file(&user_email, &file_name, size_bytes, mime_type, content_base64, folder_id).await } #[tauri::command] async fn download_server_file(user_email: String, file_id: i64) -> Result { storage::download_server_file(&user_email, file_id).await } #[tauri::command] async fn list_server_users() -> Result, String> { storage::list_server_users().await } #[tauri::command] async fn update_user_heartbeat(user_email: String) -> Result { storage::update_user_heartbeat(&user_email).await } // Admin security & user management commands #[tauri::command] async fn get_admin_security_info(admin_email: String) -> Result { admin::get_admin_security_info(&admin_email).await } #[tauri::command] async fn update_admin_security_policy( admin_email: String, device_binding: bool, trusted_device: Option, ip_binding: bool, trusted_ip: Option, ) -> Result { admin::update_admin_security_policy(&admin_email, device_binding, trusted_device, ip_binding, trusted_ip).await } #[tauri::command] async fn admin_update_user( admin_email: String, target_id: i32, new_username: Option, new_email: Option, new_role: Option, new_password: Option, admin_pin: Option, ) -> Result { admin::admin_update_user(&admin_email, target_id, new_username, new_email, new_role, new_password, admin_pin).await } #[tauri::command] async fn admin_delete_user( admin_email: String, target_id: i32, admin_pin: Option, ) -> Result { admin::admin_delete_user(&admin_email, target_id, admin_pin).await } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppReleaseInfo { pub version: String, pub current_version: String, pub body: String, pub has_update: bool, pub download_url: String, pub platform: String, pub windows_url: Option, pub tv_apk_url: Option, pub mobile_apk_url: Option, pub linux_url: Option, pub macos_url: Option, } fn detect_current_platform() -> &'static str { #[cfg(target_os = "android")] { "android" } #[cfg(target_os = "windows")] { "windows" } #[cfg(target_os = "macos")] { "macos" } #[cfg(target_os = "linux")] { "linux" } #[cfg(not(any(target_os = "android", target_os = "windows", target_os = "macos", target_os = "linux")))] { "unknown" } } fn is_newer_semver(remote: &str, current: &str) -> bool { let parse = |v: &str| -> Vec { v.trim_start_matches('v') .split('.') .map(|s| s.parse::().unwrap_or(0)) .collect() }; let r = parse(remote); let c = parse(current); for i in 0..r.len().max(c.len()) { let rv = *r.get(i).unwrap_or(&0); let cv = *c.get(i).unwrap_or(&0); if rv > cv { return true; } if rv < cv { return false; } } false } #[tauri::command] async fn check_app_update(current_version: Option) -> Result { let cur_ver = current_version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); let client = reqwest::Client::builder() .user_agent("HAS-App-Updater") .timeout(std::time::Duration::from_secs(10)) .build() .map_err(|e| e.to_string())?; let res = client .get("https://api.github.com/repos/autli5/has/releases/latest") .header("Accept", "application/vnd.github.v3+json") .send() .await .map_err(|e| format!("Failed to reach GitHub: {}", e))?; if !res.status().is_success() { return Err(format!("GitHub API returned HTTP {}", res.status())); } let json: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; let raw_tag = json["tag_name"].as_str().unwrap_or("0.0.0"); let remote_version = raw_tag.trim_start_matches('v').to_string(); let body = json["body"].as_str().unwrap_or("Новое обновление доступно.").to_string(); let html_url = json["html_url"] .as_str() .unwrap_or("https://github.com/autli5/has/releases/latest") .to_string(); let has_update = is_newer_semver(&remote_version, &cur_ver); let mut tv_apk_url = None; let mut mobile_apk_url = None; let mut windows_url = None; let mut linux_url = None; let mut macos_url = None; if let Some(assets) = json["assets"].as_array() { for asset in assets { if let (Some(name), Some(url)) = (asset["name"].as_str(), asset["browser_download_url"].as_str()) { let lower = name.to_lowercase(); if (lower.contains("_tv_") || lower.contains("-tv-") || lower.contains(".tv.")) && lower.ends_with(".apk") { tv_apk_url = Some(url.to_string()); } else if lower.ends_with(".apk") && mobile_apk_url.is_none() { mobile_apk_url = Some(url.to_string()); } else if lower.ends_with(".exe") || lower.ends_with(".msi") { if windows_url.is_none() || lower.ends_with(".exe") { windows_url = Some(url.to_string()); } } else if lower.ends_with(".deb") || lower.ends_with(".appimage") || lower.ends_with(".rpm") { if linux_url.is_none() || lower.ends_with(".deb") { linux_url = Some(url.to_string()); } } else if lower.ends_with(".dmg") { macos_url = Some(url.to_string()); } } } } if tv_apk_url.is_none() && mobile_apk_url.is_some() { tv_apk_url = mobile_apk_url.clone(); } if mobile_apk_url.is_none() && tv_apk_url.is_some() { mobile_apk_url = tv_apk_url.clone(); } let platform = detect_current_platform().to_string(); Ok(AppReleaseInfo { version: remote_version, current_version: cur_ver, body, has_update, download_url: html_url, platform, windows_url, tv_apk_url, mobile_apk_url, linux_url, macos_url, }) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let _ = rustls::crypto::ring::default_provider().install_default(); auth::load_env(); #[allow(unused_mut)] let mut builder = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_process::init()); #[cfg(desktop)] { builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); } builder .invoke_handler(tauri::generate_handler![ check_db, login, register, request_registration_code, register_with_code, validate_email, request_reset_code, verify_reset_code, reset_password, get_db_info, get_storage_stats, list_storage_items, create_server_folder, create_server_file, save_server_file_content, delete_server_item, rename_server_item, upload_server_file, download_server_file, list_server_users, update_user_heartbeat, get_admin_security_info, update_admin_security_policy, admin_update_user, admin_delete_user, profile::get_user_profile, profile::save_user_profile, cinema::get_cinema_config, cinema::get_cinema_state, cinema::save_cinema_state, cinema::update_cinema_progress, cinema::remove_cinema_history_item, cinema::clear_cinema_history, cinema::toggle_cinema_bookmark, cinema::tmdb_query, cinema::get_russian_media_info, cinema::filter_playable_media, check_app_update ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }