has/src-tauri/src/lib.rs

406 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<DbStatus, String> {
Ok(auth::check_db_health().await)
}
#[tauri::command]
async fn login(email: String, password: String) -> Result<AuthResponse, String> {
Ok(auth::authenticate_user(&email, &password).await)
}
#[tauri::command]
async fn register(email: String, password: String, username: Option<String>) -> Result<AuthResponse, String> {
Ok(auth::register_user(&email, &password, username.as_deref()).await)
}
#[tauri::command]
async fn request_registration_code(email: String, username: Option<String>) -> Result<ResetRequestResponse, String> {
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<String>,
) -> Result<AuthResponse, String> {
Ok(auth::register_with_code(&email, &code, &password, username.as_deref()).await)
}
#[tauri::command]
async fn validate_email(email: String) -> Result<bool, String> {
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<ResetRequestResponse, String> {
Ok(auth::request_reset_code(&email).await)
}
#[tauri::command]
fn verify_reset_code(email: String, code: String) -> Result<AuthResponse, String> {
Ok(auth::verify_reset_code(&email, &code))
}
#[tauri::command]
async fn reset_password(email: String, code: String, new_password: String) -> Result<AuthResponse, String> {
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<StorageStats, String> {
storage::get_server_storage_stats().await
}
#[tauri::command]
async fn list_storage_items(user_email: String, folder_id: Option<i64>) -> Result<Vec<StorageItemDto>, 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<i64>) -> Result<StorageItemDto, String> {
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<bool, String> {
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<bool, String> {
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<String>,
folder_id: Option<i64>,
) -> Result<StorageItemDto, String> {
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<bool, String> {
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<String>,
content_base64: Option<String>,
folder_id: Option<i64>,
) -> Result<StorageItemDto, String> {
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::ServerFileDownloadDto, String> {
storage::download_server_file(&user_email, file_id).await
}
#[tauri::command]
async fn list_server_users() -> Result<Vec<ServerUserDto>, String> {
storage::list_server_users().await
}
#[tauri::command]
async fn update_user_heartbeat(user_email: String) -> Result<bool, String> {
storage::update_user_heartbeat(&user_email).await
}
// Admin security & user management commands
#[tauri::command]
async fn get_admin_security_info(admin_email: String) -> Result<DeviceSecurityInfo, String> {
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<String>,
ip_binding: bool,
trusted_ip: Option<String>,
) -> Result<DeviceSecurityInfo, String> {
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<String>,
new_email: Option<String>,
new_role: Option<String>,
new_password: Option<String>,
admin_pin: Option<String>,
) -> Result<String, String> {
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<String>,
) -> Result<String, String> {
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<String>,
pub tv_apk_url: Option<String>,
pub mobile_apk_url: Option<String>,
pub linux_url: Option<String>,
pub macos_url: Option<String>,
}
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<u32> {
v.trim_start_matches('v')
.split('.')
.map(|s| s.parse::<u32>().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<String>) -> Result<AppReleaseInfo, String> {
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");
}