From 9b18b97c49d32074c89c2019845086d3bfcba055 Mon Sep 17 00:00:00 2001 From: spinline Date: Sat, 7 Feb 2026 17:17:16 +0300 Subject: [PATCH] Fetch and display actual username in sidebar profile section --- backend/src/db.rs | 22 +- backend/src/handlers/auth.rs | 15 +- frontend/src/app.rs | 14 +- frontend/src/components/layout/sidebar.rs | 264 +++++++++++++++------- frontend/src/store.rs | 93 ++++---- 5 files changed, 270 insertions(+), 138 deletions(-) diff --git a/backend/src/db.rs b/backend/src/db.rs index 4080820..c85da2a 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -59,15 +59,23 @@ impl Db { Ok(()) } - pub async fn get_user_by_username(&self, username: &str) -> Result> { - let row = sqlx::query("SELECT id, password_hash FROM users WHERE username = ?") - .bind(username) - .fetch_optional(&self.pool) - .await?; + pub async fn get_user_by_username(&self, username: &str) -> Result> { + let row = sqlx::query("SELECT id, password_hash FROM users WHERE username = ?") + .bind(username) + .fetch_optional(&self.pool) + .await?; - Ok(row.map(|r| (r.get(0), r.get(1)))) - } + Ok(row.map(|r| (r.get(0), r.get(1)))) + } + pub async fn get_username_by_id(&self, id: i64) -> Result> { + let row = sqlx::query("SELECT username FROM users WHERE id = ?") + .bind(id) + .fetch_optional(&self.pool) + .await?; + + Ok(row.map(|r| r.get(0))) + } pub async fn has_users(&self) -> Result { let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") .fetch_one(&self.pool) diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index 1dd0272..80f6330 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -15,7 +15,6 @@ pub struct LoginRequest { password: String, } -#[allow(dead_code)] #[derive(Serialize, ToSchema)] pub struct UserResponse { username: String, @@ -79,7 +78,7 @@ pub async fn login_handler( .build(); tracing::info!("Session created and cookie set for user: {}", payload.username); - (StatusCode::OK, jar.add(cookie), "Login successful").into_response() + (StatusCode::OK, jar.add(cookie), Json(UserResponse { username: payload.username })).into_response() } Ok(false) => { tracing::warn!("Login failed: Invalid password for {}", payload.username); @@ -120,7 +119,7 @@ pub async fn logout_handler( get, path = "/api/auth/check", responses( - (status = 200, description = "Authenticated"), + (status = 200, description = "Authenticated", body = UserResponse), (status = 401, description = "Not authenticated") ) )] @@ -130,7 +129,15 @@ pub async fn check_auth_handler( ) -> impl IntoResponse { if let Some(token) = jar.get("auth_token") { match state.db.get_session_user(token.value()).await { - Ok(Some(_)) => return StatusCode::OK.into_response(), + Ok(Some(user_id)) => { + // Fetch username + // We need a helper in db.rs to get username by id, or we can use a direct query here if we don't want to change db.rs interface yet. + // But better to add `get_username_by_id` to db.rs + // For now let's query directly or via a new db method. + if let Ok(Some(username)) = state.db.get_username_by_id(user_id).await { + return (StatusCode::OK, Json(UserResponse { username })).into_response(); + } + }, _ => {} // Invalid session } } diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 460de4c..ae68a9e 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -12,6 +12,11 @@ struct SetupStatus { completed: bool, } +#[derive(Deserialize)] +struct UserResponse { + username: String, +} + #[component] pub fn App() -> impl IntoView { crate::store::provide_torrent_store(); @@ -55,6 +60,14 @@ pub fn App() -> impl IntoView { Ok(resp) => { if resp.status() == 200 { logging::log!("Authenticated!"); + + // Parse user info + if let Ok(user_info) = resp.json::().await { + if let Some(store) = use_context::() { + store.user.set(Some(user_info.username)); + } + } + set_is_authenticated.set(true); } else { logging::log!("Not authenticated, redirecting to /login"); @@ -71,7 +84,6 @@ pub fn App() -> impl IntoView { set_is_loading.set(false); }); }); - // Initialize push notifications (Only if authenticated) create_effect(move |_| { if is_authenticated.get() { diff --git a/frontend/src/components/layout/sidebar.rs b/frontend/src/components/layout/sidebar.rs index 8c79f02..3d11ee6 100644 --- a/frontend/src/components/layout/sidebar.rs +++ b/frontend/src/components/layout/sidebar.rs @@ -86,88 +86,190 @@ pub fn Sidebar() -> impl IntoView { }); }; - view! { -
-
-
+ let username = move || { + + store.user.get().unwrap_or_else(|| "User".to_string()) + + }; + + + + let first_letter = move || { + + username().chars().next().unwrap_or('?').to_uppercase().to_string() + + }; + + + + view! { + +
+ +
+ + + +
+ + + +
+ +
+ +
+ +
+ + {first_letter} + +
+ +
+ +
+ +
{username}
+ +
"Online"
+ +
+ + -
-
-
-
- "A"
+
-
-
"Admin User"
-
"Online"
-
- +
-
-
- }} + + }} diff --git a/frontend/src/store.rs b/frontend/src/store.rs index 5307b41..badc122 100644 --- a/frontend/src/store.rs +++ b/frontend/src/store.rs @@ -27,9 +27,9 @@ pub fn show_toast_with_signal( message: message.into(), }; let item = NotificationItem { id, notification }; - + notifications.update(|list| list.push(item)); - + // Auto-remove after 5 seconds let _ = set_timeout( move || { @@ -120,6 +120,7 @@ pub struct TorrentStore { pub search_query: RwSignal, pub global_stats: RwSignal, pub notifications: RwSignal>, + pub user: RwSignal>, } pub fn provide_torrent_store() { @@ -128,6 +129,7 @@ pub fn provide_torrent_store() { let search_query = create_rw_signal(String::new()); let global_stats = create_rw_signal(GlobalStats::default()); let notifications = create_rw_signal(Vec::::new()); + let user = create_rw_signal(Option::::None); let store = TorrentStore { torrents, @@ -135,6 +137,7 @@ pub fn provide_torrent_store() { search_query, global_stats, notifications, + user, }; provide_context(store); @@ -149,7 +152,7 @@ pub fn provide_torrent_store() { loop { let es_result = EventSource::new("/api/events"); - + match es_result { Ok(mut es) => { match es.subscribe("message") { @@ -163,7 +166,7 @@ pub fn provide_torrent_store() { if !got_first_message { got_first_message = true; backoff_ms = 1000; // Reset backoff on real data - + if was_connected && disconnect_notified { // We were previously connected, lost connection, and now truly reconnected show_toast_with_signal( @@ -225,11 +228,11 @@ pub fn provide_torrent_store() { AppEvent::Notification(n) => { // Show toast notification show_toast_with_signal(notifications, n.level.clone(), n.message.clone()); - + // Show browser notification for critical events - let is_critical = n.message.contains("tamamlandı") + let is_critical = n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error; - + if is_critical { let title = match n.level { shared::NotificationLevel::Success => "✅ VibeTorrent", @@ -237,7 +240,7 @@ pub fn provide_torrent_store() { shared::NotificationLevel::Warning => "⚠️ VibeTorrent", shared::NotificationLevel::Info => "ℹ️ VibeTorrent", }; - + crate::utils::notification::show_notification_if_enabled( title, &n.message @@ -316,7 +319,7 @@ struct PushKeys { /// Requests notification permission if needed, then subscribes to push pub async fn subscribe_to_push_notifications() { use gloo_net::http::Request; - + // First, request notification permission if not already granted let window = web_sys::window().expect("window should exist"); let permission_granted = if let Ok(notification_class) = js_sys::Reflect::get(&window, &"Notification".into()) { @@ -324,13 +327,13 @@ pub async fn subscribe_to_push_notifications() { log::error!("Notification API not available"); return; } - + // Check current permission let current_permission = js_sys::Reflect::get(¬ification_class, &"permission".into()) .ok() .and_then(|p| p.as_string()) .unwrap_or_default(); - + if current_permission == "granted" { log::info!("Notification permission already granted"); true @@ -376,14 +379,14 @@ pub async fn subscribe_to_push_notifications() { log::error!("Cannot access Notification class"); return; }; - + if !permission_granted { log::warn!("Notification permission not granted, cannot subscribe to push"); return; } - + log::info!("Notification permission granted! Proceeding with push subscription..."); - + // Get VAPID public key from backend let public_key_response = match Request::get("/api/push/public-key").send().await { Ok(resp) => resp, @@ -392,7 +395,7 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let public_key_data: serde_json::Value = match public_key_response.json().await { Ok(data) => data, Err(e) => { @@ -400,7 +403,7 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let public_key = match public_key_data.get("publicKey").and_then(|v| v.as_str()) { Some(key) => key, None => { @@ -408,9 +411,9 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + log::info!("VAPID public key from backend: {} (len: {})", public_key, public_key.len()); - + // Convert VAPID public key to Uint8Array let public_key_array = match url_base64_to_uint8array(public_key) { Ok(arr) => { @@ -422,12 +425,12 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + // Get service worker registration let window = web_sys::window().expect("window should exist"); let navigator = window.navigator(); let service_worker = navigator.service_worker(); - + let registration_promise = match service_worker.ready() { Ok(promise) => promise, Err(e) => { @@ -435,9 +438,9 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let registration_future = wasm_bindgen_futures::JsFuture::from(registration_promise); - + let registration = match registration_future.await { Ok(reg) => reg, Err(e) => { @@ -445,11 +448,11 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let service_worker_registration = registration .dyn_into::() .expect("should be ServiceWorkerRegistration"); - + // Subscribe to push let push_manager = match service_worker_registration.push_manager() { Ok(pm) => pm, @@ -458,11 +461,11 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let subscribe_options = web_sys::PushSubscriptionOptionsInit::new(); subscribe_options.set_user_visible_only(true); subscribe_options.set_application_server_key(&public_key_array); - + let subscribe_promise = match push_manager.subscribe_with_options(&subscribe_options) { Ok(promise) => promise, Err(e) => { @@ -470,9 +473,9 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let subscription_future = wasm_bindgen_futures::JsFuture::from(subscribe_promise); - + let subscription = match subscription_future.await { Ok(sub) => sub, Err(e) => { @@ -480,11 +483,11 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let push_subscription = subscription .dyn_into::() .expect("should be PushSubscription"); - + // Get subscription JSON using toJSON() method let json_result = match js_sys::Reflect::get(&push_subscription, &"toJSON".into()) { Ok(func) if func.is_function() => { @@ -502,7 +505,7 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let json_value = match js_sys::JSON::stringify(&json_result) { Ok(val) => val, Err(e) => { @@ -510,11 +513,11 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + let subscription_json_str = json_value.as_string().expect("should be string"); - + log::info!("Push subscription: {}", subscription_json_str); - + // Parse and send to backend let subscription_data: serde_json::Value = match serde_json::from_str(&subscription_json_str) { Ok(data) => data, @@ -523,35 +526,35 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + // Extract endpoint and keys let endpoint = subscription_data .get("endpoint") .and_then(|v| v.as_str()) .expect("endpoint should exist") .to_string(); - + let keys_obj = subscription_data .get("keys") .expect("keys should exist"); - + let p256dh = keys_obj .get("p256dh") .and_then(|v| v.as_str()) .expect("p256dh should exist") .to_string(); - + let auth = keys_obj .get("auth") .and_then(|v| v.as_str()) .expect("auth should exist") .to_string(); - + let push_data = PushSubscriptionData { endpoint, keys: PushKeys { p256dh, auth }, }; - + // Send to backend let response = match Request::post("/api/push/subscribe") .json(&push_data) @@ -565,7 +568,7 @@ pub async fn subscribe_to_push_notifications() { return; } }; - + if response.ok() { log::info!("Successfully subscribed to push notifications"); } else { @@ -580,10 +583,10 @@ fn url_base64_to_uint8array(base64_string: &str) -> Result Result()?; - + Ok(array) }