406 lines
13 KiB
Rust
406 lines
13 KiB
Rust
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");
|
||
}
|