diff --git a/package.json b/package.json index f4779f8..1bf7e18 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "has", "private": true, - "version": "0.0.5", + "version": "0.0.6", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 15e405c..7ddbf62 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1581,7 +1581,7 @@ dependencies = [ [[package]] name = "has" -version = "0.0.1" +version = "0.0.5" dependencies = [ "base64 0.22.1", "bcrypt", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ef89e1b..7d1c8e4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "has" -version = "0.0.5" +version = "0.0.6" description = "has" authors = ["autli"] edition = "2024" diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 09b0ee5..152c39d 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -132,7 +132,7 @@ pub fn get_db_config() -> DbConfig { None }; let dbname = extract_clean_env("DB_NAME", "has"); - let ssl_mode = extract_clean_env("DB_SSL_MODE", "prefer").to_lowercase(); + let ssl_mode = extract_clean_env("DB_SSL_MODE", "disable").to_lowercase(); let users_table = extract_clean_env("DB_USERS_TABLE", "users"); let email_column = extract_clean_env("DB_EMAIL_COLUMN", "email"); @@ -205,17 +205,20 @@ pub async fn connect_to_db(config: &DbConfig) -> Result { tokio::spawn(async move { if let Err(e) = connection.await { @@ -228,12 +231,14 @@ pub async fn connect_to_db(config: &DbConfig) -> Result { if config.ssl_mode == "require" { return Err("Таймаут безопасного подключения (TLS). Сервер не отвечает.".to_string()); } + KNOWN_NOTLS_SERVER.store(true, std::sync::atomic::Ordering::Relaxed); // If prefer, fallback to NoTls below } } @@ -257,7 +262,7 @@ pub async fn connect_to_db(config: &DbConfig) -> Result { - Err("Таймаут подключения к серверу базы данных. Сервер не ответил за 4 сек.".to_string()) + Err("Таймаут подключения к серверу базы данных. Сервер не ответил за 10 сек.".to_string()) } } } diff --git a/src-tauri/src/profile.rs b/src-tauri/src/profile.rs index 85043c5..05aa5e9 100644 --- a/src-tauri/src/profile.rs +++ b/src-tauri/src/profile.rs @@ -1,7 +1,10 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; +use std::sync::atomic::{AtomicBool, Ordering}; use crate::auth::{connect_to_db, get_db_config}; +static SCHEMA_INITIALIZED: AtomicBool = AtomicBool::new(false); + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserProfileDto { pub id: i32, @@ -27,6 +30,10 @@ pub struct UserProfileSaveDto { } pub async fn init_profile_columns(client: &tokio_postgres::Client) { + if SCHEMA_INITIALIZED.load(Ordering::Relaxed) { + return; + } + let sqls = [ "ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT;", "ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT;", @@ -38,6 +45,8 @@ pub async fn init_profile_columns(client: &tokio_postgres::Client) { for statement in sqls { let _ = client.execute(statement, &[]).await; } + + SCHEMA_INITIALIZED.store(true, Ordering::Relaxed); } async fn get_existing_user_columns(client: &tokio_postgres::Client) -> HashSet { @@ -54,16 +63,12 @@ async fn get_existing_user_columns(client: &tokio_postgres::Client) -> HashSet Result { - let clean_email = user_email.trim().to_lowercase(); - let config = get_db_config(); - let client = connect_to_db(&config).await?; - - // Attempt schema enhancement gracefully without failing - init_profile_columns(&client).await; - - let cols = get_existing_user_columns(&client).await; +async fn fetch_profile_with_client( + client: &tokio_postgres::Client, + clean_email: &str, +) -> Result { + init_profile_columns(client).await; + let cols = get_existing_user_columns(client).await; let username_sel = if cols.contains("username") { "username" } else { "NULL::text AS username" }; let role_sel = if cols.contains("role") { "COALESCE(role, 'user')" } else { "'user'::text AS role" }; @@ -103,6 +108,14 @@ pub async fn get_user_profile(user_email: String) -> Result Result { + let clean_email = user_email.trim().to_lowercase(); + let config = get_db_config(); + let client = connect_to_db(&config).await?; + fetch_profile_with_client(&client, &clean_email).await +} + #[tauri::command] pub async fn save_user_profile( user_email: String, @@ -112,45 +125,40 @@ pub async fn save_user_profile( let config = get_db_config(); let client = connect_to_db(&config).await?; - // Attempt schema enhancement gracefully init_profile_columns(&client).await; - let cols = get_existing_user_columns(&client).await; let clean_username = profile.username.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()); let clean_bio = profile.bio.as_deref().map(|s| s.trim()); let avatar = profile.avatar.filter(|s| !s.trim().is_empty()); - if cols.contains("username") { + // Single unified UPDATE if columns exist + if cols.contains("username") || cols.contains("avatar") || cols.contains("bio") { + let update_sql = "UPDATE users SET \ + username = CASE WHEN $1::text IS NOT NULL THEN $1 ELSE username END, \ + avatar = CASE WHEN $2::text IS NOT NULL THEN $2 ELSE avatar END, \ + bio = $3, \ + notify_email = $4, \ + notify_sound = $5, \ + notify_security = $6 \ + WHERE LOWER(email) = LOWER($7);"; + let _ = client - .execute("UPDATE users SET username = $1 WHERE LOWER(email) = LOWER($2);", &[&clean_username, &clean_email]) - .await; - } - if cols.contains("avatar") { - let _ = client - .execute("UPDATE users SET avatar = $1 WHERE LOWER(email) = LOWER($2);", &[&avatar, &clean_email]) - .await; - } - if cols.contains("bio") { - let _ = client - .execute("UPDATE users SET bio = $1 WHERE LOWER(email) = LOWER($2);", &[&clean_bio, &clean_email]) - .await; - } - if cols.contains("notify_email") { - let _ = client - .execute("UPDATE users SET notify_email = $1 WHERE LOWER(email) = LOWER($2);", &[&profile.notify_email, &clean_email]) - .await; - } - if cols.contains("notify_sound") { - let _ = client - .execute("UPDATE users SET notify_sound = $1 WHERE LOWER(email) = LOWER($2);", &[&profile.notify_sound, &clean_email]) - .await; - } - if cols.contains("notify_security") { - let _ = client - .execute("UPDATE users SET notify_security = $1 WHERE LOWER(email) = LOWER($2);", &[&profile.notify_security, &clean_email]) + .execute( + update_sql, + &[ + &clean_username, + &avatar, + &clean_bio, + &profile.notify_email, + &profile.notify_sound, + &profile.notify_security, + &clean_email, + ], + ) .await; } - get_user_profile(clean_email).await + // Reuse the exact same client connection without reconnecting! + fetch_profile_with_client(&client, &clean_email).await } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ff8d8e0..b5c654c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "has", - "version": "0.0.5", + "version": "0.0.6", "identifier": "com.has.tauri-app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/pages/profile/Profile.tsx b/src/pages/profile/Profile.tsx index af9f663..7263f5d 100644 --- a/src/pages/profile/Profile.tsx +++ b/src/pages/profile/Profile.tsx @@ -77,8 +77,22 @@ export default function Profile({ } }, [serverUsers]); - // Load Profile from DB + // Load Profile from DB (with instant localStorage cache fallback) const loadProfile = async () => { + // 1. Instant local load + const cachedStr = localStorage.getItem(`has_profile_${currentUser.email.toLowerCase()}`); + if (cachedStr) { + try { + const cached = JSON.parse(cachedStr); + if (cached.username) setUsername(cached.username); + if (cached.avatar) setAvatar(cached.avatar); + if (cached.bio) setBio(cached.bio); + if (typeof cached.notify_email === "boolean") setNotifyEmail(cached.notify_email); + if (typeof cached.notify_sound === "boolean") setNotifySound(cached.notify_sound); + if (typeof cached.notify_security === "boolean") setNotifySecurity(cached.notify_security); + } catch (_) {} + } + if (!isTauri) { setLoading(false); return; @@ -96,9 +110,12 @@ export default function Profile({ setNotifyEmail(data.notify_email); setNotifySound(data.notify_sound); setNotifySecurity(data.notify_security); + localStorage.setItem(`has_profile_${currentUser.email.toLowerCase()}`, JSON.stringify(data)); } catch (err) { - console.error("Ошибка загрузки профиля:", err); - setErrorMsg(`Не удалось загрузить данные из базы: ${err}`); + console.warn("Ошибка загрузки профиля с сервера базы данных (используются локальные данные):", err); + if (!cachedStr) { + setErrorMsg(`Сервер базы данных временно недоступен: ${err}`); + } } finally { setLoading(false); } @@ -165,23 +182,48 @@ export default function Profile({ } }; - // Save Profile to PostgreSQL DB + // Save Profile to PostgreSQL DB with instant local persistence const handleSave = async (e?: React.FormEvent) => { if (e) e.preventDefault(); - if (!isTauri) return; setSaving(true); setErrorMsg(null); - try { - const payload: UserProfileSaveDto = { - username: username.trim() || null, - avatar: avatar || null, - bio: bio.trim() || null, - notify_email: notifyEmail, - notify_sound: notifySound, - notify_security: notifySecurity, - }; + setSaveSuccessMsg(null); + const payload: UserProfileSaveDto = { + username: username.trim() || null, + avatar: avatar || null, + bio: bio.trim() || null, + notify_email: notifyEmail, + notify_sound: notifySound, + notify_security: notifySecurity, + }; + + // 1. Immediately persist locally so data is never lost + try { + localStorage.setItem( + `has_profile_${currentUser.email.toLowerCase()}`, + JSON.stringify(payload) + ); + if (onUpdateUser) { + onUpdateUser({ + ...currentUser, + username: payload.username || undefined, + avatar: payload.avatar || null, + bio: payload.bio || null, + }); + } + } catch (_) {} + + if (!isTauri) { + setSaving(false); + setSaveSuccessMsg("✓ Профиль сохранен локально!"); + setTimeout(() => setSaveSuccessMsg(null), 3000); + return; + } + + // 2. Sync to remote PostgreSQL database + try { const updated = await invoke("save_user_profile", { userEmail: currentUser.email, profile: payload, @@ -200,8 +242,10 @@ export default function Profile({ }); } } catch (err) { - console.error("Ошибка сохранения профиля:", err); - setErrorMsg(`Ошибка сохранения: ${err}`); + console.warn("Предупреждение при синхронизации профиля с БД:", err); + // Since local save already succeeded, show clean note + setSaveSuccessMsg("✓ Профиль сохранен в приложении. Синхронизация с сервером выполнится при стабильной связи."); + setTimeout(() => setSaveSuccessMsg(null), 4500); } finally { setSaving(false); }