diff --git a/Cargo.lock b/Cargo.lock index 3b15cc3..bb00932 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1218,6 +1218,7 @@ dependencies = [ "serde_json", "shared", "tailwind_fuse", + "thiserror 2.0.18", "uuid", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index c63de11..491e8aa 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -30,3 +30,4 @@ base64 = "0.22.1" serde-wasm-bindgen = "0.6.5" leptos-use = "0.13" codee = "0.2" +thiserror = "2.0" diff --git a/frontend/src/api/mod.rs b/frontend/src/api/mod.rs new file mode 100644 index 0000000..8b5c339 --- /dev/null +++ b/frontend/src/api/mod.rs @@ -0,0 +1,243 @@ +use gloo_net::http::Request; +use shared::{AddTorrentRequest, TorrentActionRequest}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ApiError { + #[error("Network error")] + Network, + #[error("Server error: {status}")] + Server { status: u16 }, + #[error("Login failed")] + LoginFailed, + #[error("Unauthorized")] + Unauthorized, + #[error("Too many requests")] + RateLimited, +} + +fn base_url() -> String { + "/api".to_string() +} + +pub mod auth { + use super::*; + + #[derive(serde::Serialize)] + pub struct LoginRequest { + pub username: String, + pub password: String, + pub remember_me: bool, + } + + pub async fn login( + username: &str, + password: &str, + remember_me: bool, + ) -> Result<(), ApiError> { + let req = LoginRequest { + username: username.to_string(), + password: password.to_string(), + remember_me, + }; + let resp = Request::post(&format!("{}/auth/login", base_url())) + .json(&req) + .map_err(|_| ApiError::Network)? + .send() + .await + .map_err(|_| ApiError::Network)?; + + if resp.ok() { + Ok(()) + } else if resp.status() == 429 { + Err(ApiError::RateLimited) + } else { + Err(ApiError::LoginFailed) + } + } + + pub async fn logout() -> Result<(), ApiError> { + Request::post(&format!("{}/auth/logout", base_url())) + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(()) + } + + pub async fn check_auth() -> Result { + let resp = Request::get(&format!("{}/auth/check", base_url())) + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(resp.ok()) + } + + #[derive(serde::Deserialize)] + pub struct UserResponse { + pub username: String, + } + + pub async fn get_user() -> Result { + let resp = Request::get(&format!("{}/auth/check", base_url())) + .send() + .await + .map_err(|_| ApiError::Network)?; + let user = resp.json().await.map_err(|_| ApiError::Network)?; + Ok(user) + } +} + +pub mod setup { + use super::*; + + #[derive(serde::Serialize)] + pub struct SetupRequest { + pub username: String, + pub password: String, + } + + #[derive(serde::Deserialize)] + pub struct SetupStatusResponse { + pub completed: bool, + } + + pub async fn get_status() -> Result { + let resp = Request::get(&format!("{}/setup/status", base_url())) + .send() + .await + .map_err(|_| ApiError::Network)?; + let status = resp.json().await.map_err(|_| ApiError::Network)?; + Ok(status) + } + + pub async fn setup(username: &str, password: &str) -> Result<(), ApiError> { + let req = SetupRequest { + username: username.to_string(), + password: password.to_string(), + }; + Request::post(&format!("{}/setup", base_url())) + .json(&req) + .map_err(|_| ApiError::Network)? + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(()) + } +} + +pub mod settings { + use super::*; + use shared::GlobalLimitRequest; + + pub async fn set_global_limits(req: &GlobalLimitRequest) -> Result<(), ApiError> { + Request::post(&format!("{}/settings/global-limits", base_url())) + .json(req) + .map_err(|_| ApiError::Network)? + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(()) + } +} + +pub mod push { + use super::*; + use crate::store::PushSubscriptionData; + + pub async fn get_public_key() -> Result { + let resp = Request::get(&format!("{}/push/public-key", base_url())) + .send() + .await + .map_err(|_| ApiError::Network)?; + let key = resp.text().await.map_err(|_| ApiError::Network)?; + Ok(key) + } + + pub async fn subscribe(req: &PushSubscriptionData) -> Result<(), ApiError> { + Request::post(&format!("{}/push/subscribe", base_url())) + .json(req) + .map_err(|_| ApiError::Network)? + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(()) + } +} + +pub mod torrent { + use super::*; + + pub async fn add(uri: &str) -> Result<(), ApiError> { + let req = AddTorrentRequest { + uri: uri.to_string(), + }; + Request::post(&format!("{}/torrents/add", base_url())) + .json(&req) + .map_err(|_| ApiError::Network)? + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(()) + } + + pub async fn action(hash: &str, action: &str) -> Result<(), ApiError> { + let req = TorrentActionRequest { + hash: hash.to_string(), + action: action.to_string(), + }; + Request::post(&format!("{}/torrents/action", base_url())) + .json(&req) + .map_err(|_| ApiError::Network)? + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(()) + } + + pub async fn delete(hash: &str) -> Result<(), ApiError> { + action(hash, "delete").await + } + + pub async fn delete_with_data(hash: &str) -> Result<(), ApiError> { + action(hash, "delete_with_data").await + } + + pub async fn start(hash: &str) -> Result<(), ApiError> { + action(hash, "start").await + } + + pub async fn stop(hash: &str) -> Result<(), ApiError> { + action(hash, "stop").await + } + + pub async fn set_label(hash: &str, label: &str) -> Result<(), ApiError> { + use shared::SetLabelRequest; + let req = SetLabelRequest { + hash: hash.to_string(), + label: label.to_string(), + }; + Request::post(&format!("{}/torrents/set_label", base_url())) + .json(&req) + .map_err(|_| ApiError::Network)? + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(()) + } + + pub async fn set_priority(hash: &str, file_index: u32, priority: u8) -> Result<(), ApiError> { + use shared::SetFilePriorityRequest; + let req = SetFilePriorityRequest { + hash: hash.to_string(), + file_index, + priority, + }; + Request::post(&format!("{}/torrents/set_priority", base_url())) + .json(&req) + .map_err(|_| ApiError::Network)? + .send() + .await + .map_err(|_| ApiError::Network)?; + Ok(()) + } +} diff --git a/frontend/src/app.rs b/frontend/src/app.rs index b4351bf..252cbd5 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -3,100 +3,80 @@ use crate::components::toast::ToastContainer; use crate::components::torrent::table::TorrentTable; use crate::components::auth::login::Login; use crate::components::auth::setup::Setup; +use crate::api; use leptos::*; use leptos_router::*; -use serde::Deserialize; - -#[derive(Deserialize)] -struct SetupStatus { - completed: bool, -} - -#[derive(Deserialize)] -struct UserResponse { - username: String, -} #[component] pub fn App() -> impl IntoView { crate::store::provide_torrent_store(); - // Auth State let (is_loading, set_is_loading) = create_signal(true); let (is_authenticated, set_is_authenticated) = create_signal(false); - // Check Auth & Setup Status on load create_effect(move |_| { spawn_local(async move { logging::log!("App initialization started..."); - // 1. Check Setup Status - let setup_res = gloo_net::http::Request::get("/api/setup/status").send().await; + let setup_res = api::setup::get_status().await; match setup_res { - Ok(resp) => { - if resp.ok() { - match resp.json::().await { - Ok(status) => { - if !status.completed { - logging::log!("Setup not completed, redirecting to /setup"); - let navigate = use_navigate(); - navigate("/setup", Default::default()); - set_is_loading.set(false); - return; - } - } - Err(e) => logging::error!("Failed to parse setup status: {}", e), - } + Ok(status) => { + if !status.completed { + logging::log!("Setup not completed, redirecting to /setup"); + let navigate = use_navigate(); + navigate("/setup", Default::default()); + set_is_loading.set(false); + return; } } - Err(e) => logging::error!("Network error checking setup status: {}", e), + Err(e) => logging::error!("Failed to get setup status: {:?}", e), } - // 2. Check Auth Status - let auth_res = gloo_net::http::Request::get("/api/auth/check").send().await; + let auth_res = api::auth::check_auth().await; - match auth_res { - Ok(resp) => { - if resp.status() == 200 { - logging::log!("Authenticated!"); + match auth_res { + Ok(true) => { + 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); - - // If user is already authenticated but on login/setup page, redirect to home - let pathname = window().location().pathname().unwrap_or_default(); - if pathname == "/login" || pathname == "/setup" { - logging::log!("Already authenticated, redirecting to home"); - let navigate = use_navigate(); - navigate("/", Default::default()); - } - } else { - logging::log!("Not authenticated, redirecting to /login"); - let navigate = use_navigate(); - let pathname = window().location().pathname().unwrap_or_default(); - if pathname != "/login" && pathname != "/setup" { - navigate("/login", Default::default()); - } - } - } - Err(e) => logging::error!("Network error checking auth status: {}", e), + if let Ok(user_info) = api::auth::get_user().await { + if let Some(store) = use_context::() { + store.user.set(Some(user_info.username)); } + } + + set_is_authenticated.set(true); + + let pathname = window().location().pathname().unwrap_or_default(); + if pathname == "/login" || pathname == "/setup" { + logging::log!("Already authenticated, redirecting to home"); + let navigate = use_navigate(); + navigate("/", Default::default()); + } + } + Ok(false) => { + logging::log!("Not authenticated"); + + let pathname = window().location().pathname().unwrap_or_default(); + if pathname != "/login" && pathname != "/setup" { + let navigate = use_navigate(); + navigate("/login", Default::default()); + } + } + Err(e) => { + logging::error!("Auth check failed: {:?}", e); + let navigate = use_navigate(); + navigate("/login", Default::default()); + } + } + set_is_loading.set(false); }); }); - // Initialize push notifications (Only if authenticated) + create_effect(move |_| { if is_authenticated.get() { spawn_local(async { - // ... (Push notification logic kept same, shortened for brevity in this replace) - // Wait a bit for service worker to be ready gloo_timers::future::TimeoutFuture::new(2000).await; if crate::utils::platform::supports_push_notifications() && !crate::utils::platform::is_safari() { diff --git a/frontend/src/components/auth/login.rs b/frontend/src/components/auth/login.rs index 45e2de0..ce8a900 100644 --- a/frontend/src/components/auth/login.rs +++ b/frontend/src/components/auth/login.rs @@ -1,12 +1,5 @@ use leptos::*; -use serde::Serialize; - -#[derive(Serialize)] -struct LoginRequest { - username: String, - password: String, - remember_me: bool, -} +use crate::api; #[component] pub fn Login() -> impl IntoView { @@ -23,35 +16,31 @@ pub fn Login() -> impl IntoView { logging::log!("Attempting login for user: {}", username.get()); + let username = username.get(); + let password = password.get(); + let remember_me = remember_me.get(); + spawn_local(async move { - let req = LoginRequest { - username: username.get(), - password: password.get(), - remember_me: remember_me.get(), - }; - - let client = gloo_net::http::Request::post("/api/auth/login") - .json(&req) - .expect("Failed to create request"); - - match client.send().await { - Ok(resp) => { - logging::log!("Login response status: {}", resp.status()); - if resp.ok() { - logging::log!("Login successful, redirecting..."); - // Force a full reload to re-run auth checks in App.rs - let _ = window().location().set_href("/"); - } else if resp.status() == 429 { - set_error.set(Some("Çok fazla başarısız deneme yaptınız. Lütfen bir süre bekleyip tekrar deneyin.".to_string())); - } else { - let text = resp.text().await.unwrap_or_default(); - logging::error!("Login failed: {}", text); - set_error.set(Some("Kullanıcı adı veya şifre hatalı".to_string())); - } + match api::auth::login(&username, &password, remember_me).await { + Ok(_) => { + logging::log!("Login successful, redirecting..."); + let _ = window().location().set_href("/"); } Err(e) => { - logging::error!("Network error: {}", e); - set_error.set(Some("Bağlantı hatası".to_string())); + logging::error!("Login failed: {:?}", e); + let msg = match e { + crate::api::ApiError::RateLimited => { + "Çok fazla başarısız deneme yaptınız. Lütfen bir süre bekleyip tekrar deneyin.".to_string() + } + crate::api::ApiError::Unauthorized | crate::api::ApiError::LoginFailed => { + "Kullanıcı adı veya şifre hatalı".to_string() + } + crate::api::ApiError::Network => { + "Bağlantı hatası".to_string() + } + _ => "Bir hata oluştu".to_string() + }; + set_error.set(Some(msg)); } } set_loading.set(false); diff --git a/frontend/src/components/auth/setup.rs b/frontend/src/components/auth/setup.rs index ac71c3a..ca6d2e9 100644 --- a/frontend/src/components/auth/setup.rs +++ b/frontend/src/components/auth/setup.rs @@ -1,11 +1,5 @@ use leptos::*; -use serde::Serialize; - -#[derive(Serialize)] -struct SetupRequest { - username: String, - password: String, -} +use crate::api; #[component] pub fn Setup() -> impl IntoView { @@ -35,29 +29,18 @@ pub fn Setup() -> impl IntoView { return; } + let username = username.get(); + let password = pass; + spawn_local(async move { - let req = SetupRequest { - username: username.get(), - password: pass, - }; - - let client = gloo_net::http::Request::post("/api/setup") - .json(&req) - .expect("Failed to create request"); - - match client.send().await { - Ok(resp) => { - if resp.ok() { - // Redirect to home after setup (auto-login handled by backend) - // Full reload to ensure auth state is refreshed - let _ = window().location().set_href("/"); - } else { - let text = resp.text().await.unwrap_or_default(); - set_error.set(Some(format!("Hata: {}", text))); - } + match api::setup::setup(&username, &password).await { + Ok(_) => { + logging::log!("Setup completed successfully, redirecting..."); + let _ = window().location().set_href("/"); } - Err(_) => { - set_error.set(Some("Bağlantı hatası".to_string())); + Err(e) => { + logging::error!("Setup failed: {:?}", e); + set_error.set(Some("Kurulum başarısız oldu".to_string())); } } set_loading.set(false); diff --git a/frontend/src/components/layout/sidebar.rs b/frontend/src/components/layout/sidebar.rs index 3d11ee6..1a0568b 100644 --- a/frontend/src/components/layout/sidebar.rs +++ b/frontend/src/components/layout/sidebar.rs @@ -1,5 +1,6 @@ use leptos::wasm_bindgen::JsCast; use leptos::*; +use crate::api; #[component] pub fn Sidebar() -> impl IntoView { @@ -76,12 +77,8 @@ pub fn Sidebar() -> impl IntoView { let handle_logout = move |_| { spawn_local(async move { - let client = gloo_net::http::Request::post("/api/auth/logout"); - if let Ok(resp) = client.send().await { - if resp.ok() { - // Force full reload to clear state - let _ = window().location().set_href("/login"); - } + if api::auth::logout().await.is_ok() { + let _ = window().location().set_href("/login"); } }); }; diff --git a/frontend/src/components/layout/statusbar.rs b/frontend/src/components/layout/statusbar.rs index 0738660..cc20541 100644 --- a/frontend/src/components/layout/statusbar.rs +++ b/frontend/src/components/layout/statusbar.rs @@ -2,6 +2,7 @@ use leptos::*; use leptos_use::storage::use_local_storage; use codee::string::FromToStringCodec; use shared::GlobalLimitRequest; +use crate::api; fn format_bytes(bytes: i64) -> String { const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; @@ -60,38 +61,23 @@ pub fn StatusBar() -> impl IntoView { let limit_type = limit_type.to_string(); logging::log!("Setting {} limit to {}", limit_type, val); + let req = if limit_type == "down" { + GlobalLimitRequest { + max_download_rate: Some(val), + max_upload_rate: None, + } + } else { + GlobalLimitRequest { + max_download_rate: None, + max_upload_rate: Some(val), + } + }; + spawn_local(async move { - let req_body = if limit_type == "down" { - GlobalLimitRequest { - max_download_rate: Some(val), - max_upload_rate: None, - } + if let Err(e) = api::settings::set_global_limits(&req).await { + logging::error!("Failed to set limit: {:?}", e); } else { - GlobalLimitRequest { - max_download_rate: None, - max_upload_rate: Some(val), - } - }; - - let client = - gloo_net::http::Request::post("/api/settings/global-limits").json(&req_body); - - match client { - Ok(req) => match req.send().await { - Ok(resp) => { - if !resp.ok() { - logging::error!( - "Failed to set limit: {} {}", - resp.status(), - resp.status_text() - ); - } else { - logging::log!("Limit set successfully"); - } - } - Err(e) => logging::error!("Network error setting limit: {}", e), - }, - Err(e) => logging::error!("Failed to create request: {}", e), + logging::log!("Limit set successfully"); } }); }; diff --git a/frontend/src/components/torrent/add_torrent.rs b/frontend/src/components/torrent/add_torrent.rs index ba6c8e6..428fbdd 100644 --- a/frontend/src/components/torrent/add_torrent.rs +++ b/frontend/src/components/torrent/add_torrent.rs @@ -1,7 +1,8 @@ use leptos::*; use leptos::html::Dialog; use crate::store::{show_toast_with_signal, TorrentStore}; -use shared::{AddTorrentRequest, NotificationLevel}; +use crate::api; +use shared::NotificationLevel; #[component] @@ -17,7 +18,6 @@ pub fn AddTorrentModal( let (is_loading, set_loading) = create_signal(false); let (error_msg, set_error_msg) = create_signal(Option::::None); - // Effect to open the dialog when the component mounts/renders create_effect(move |_| { if let Some(dialog) = dialog_ref.get() { let _ = dialog.show_modal(); @@ -35,44 +35,22 @@ pub fn AddTorrentModal( set_loading.set(true); set_error_msg.set(None); + let uri_val = uri_val; spawn_local(async move { - let req_body = AddTorrentRequest { uri: uri_val }; - - match gloo_net::http::Request::post("/api/torrents/add") - .json(&req_body) - { - Ok(req) => { - match req.send().await { - Ok(resp) => { - if resp.ok() { - logging::log!("Torrent added successfully"); - show_toast_with_signal(notifications, NotificationLevel::Success, "Torrent eklendi"); - set_loading.set(false); - if let Some(dialog) = dialog_ref.get() { - dialog.close(); - } - on_close.call(()); - } else { - let status = resp.status(); - let text = resp.text().await.unwrap_or_default(); - logging::error!("Failed to add torrent: {} - {}", status, text); - show_toast_with_signal(notifications, NotificationLevel::Error, "Torrent eklenemedi"); - set_error_msg.set(Some(format!("Error {}: {}", status, text))); - set_loading.set(false); - } - } - Err(e) => { - logging::error!("Network error: {}", e); - show_toast_with_signal(notifications, NotificationLevel::Error, "Bağlantı hatası"); - set_error_msg.set(Some(format!("Network Error: {}", e))); - set_loading.set(false); - } + match api::torrent::add(&uri_val).await { + Ok(_) => { + logging::log!("Torrent added successfully"); + show_toast_with_signal(notifications, NotificationLevel::Success, "Torrent eklendi"); + set_loading.set(false); + if let Some(dialog) = dialog_ref.get() { + dialog.close(); } + on_close.call(()); } Err(e) => { - logging::error!("Serialization error: {}", e); - show_toast_with_signal(notifications, NotificationLevel::Error, "İstek hatası"); - set_error_msg.set(Some(format!("Request Error: {}", e))); + logging::error!("Failed to add torrent: {:?}", e); + show_toast_with_signal(notifications, NotificationLevel::Error, "Torrent eklenemedi"); + set_error_msg.set(Some(format!("Error: {:?}", e))); set_loading.set(false); } } diff --git a/frontend/src/components/torrent/table.rs b/frontend/src/components/torrent/table.rs index 09c9ed4..0f2ba6e 100644 --- a/frontend/src/components/torrent/table.rs +++ b/frontend/src/components/torrent/table.rs @@ -1,6 +1,7 @@ use leptos::*; use leptos_use::{on_click_outside, use_timeout_fn}; use crate::store::{get_action_messages, show_toast_with_signal}; +use crate::api; use shared::NotificationLevel; fn format_bytes(bytes: i64) -> String { @@ -201,54 +202,33 @@ pub fn TorrentTable() -> impl IntoView { let on_action = move |(action, hash): (String, String)| { logging::log!("TorrentTable Action: {} on {}", action, hash); - // Note: Don't close menu here - ContextMenu's on_close handles it - // Closing here would dispose ContextMenu while still in callback chain - // Get action messages for toast (Clean Code: DRY) let (success_msg, error_msg) = get_action_messages(&action); let success_msg = success_msg.to_string(); let error_msg = error_msg.to_string(); - // Capture notifications signal before async (use_context unavailable in spawn_local) let notifications = store.notifications; + let hash = hash.clone(); + let action = action.clone(); + spawn_local(async move { - let action_req = if action == "delete_with_data" { - "delete_with_data" - } else { - &action + let result = match action.as_str() { + "delete" => api::torrent::delete(&hash).await, + "delete_with_data" => api::torrent::delete_with_data(&hash).await, + "start" => api::torrent::start(&hash).await, + "stop" => api::torrent::stop(&hash).await, + _ => api::torrent::action(&hash, &action).await, }; - let req_body = shared::TorrentActionRequest { - hash: hash.clone(), - action: action_req.to_string(), - }; - - let client = gloo_net::http::Request::post("/api/torrents/action").json(&req_body); - - match client { - Ok(req) => match req.send().await { - Ok(resp) => { - if !resp.ok() { - logging::error!( - "Failed to execute action: {} {}", - resp.status(), - resp.status_text() - ); - show_toast_with_signal(notifications, NotificationLevel::Error, error_msg); - } else { - logging::log!("Action {} executed successfully", action); - show_toast_with_signal(notifications, NotificationLevel::Success, success_msg); - } - } - Err(e) => { - logging::error!("Network error executing action: {}", e); - show_toast_with_signal(notifications, NotificationLevel::Error, format!("{}: Bağlantı hatası", error_msg)); - } - }, + match result { + Ok(_) => { + logging::log!("Action {} executed successfully", action); + show_toast_with_signal(notifications, NotificationLevel::Success, success_msg); + } Err(e) => { - logging::error!("Failed to serialize request: {}", e); - show_toast_with_signal(notifications, NotificationLevel::Error, error_msg); + logging::error!("Action failed: {:?}", e); + show_toast_with_signal(notifications, NotificationLevel::Error, format!("{}: {:?}", error_msg, e)); } } }); diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 9ea112d..35ddb24 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -1,8 +1,8 @@ mod app; -// mod models; // Removed mod components; pub mod utils; pub mod store; +pub mod api; use leptos::*; use wasm_bindgen::prelude::*; diff --git a/frontend/src/store.rs b/frontend/src/store.rs index cb26093..a525905 100644 --- a/frontend/src/store.rs +++ b/frontend/src/store.rs @@ -314,21 +314,21 @@ use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; #[derive(Serialize, Deserialize, Debug)] -struct PushSubscriptionData { - endpoint: String, - keys: PushKeys, +pub struct PushSubscriptionData { + pub endpoint: String, + pub keys: PushKeys, } #[derive(Serialize, Deserialize, Debug)] -struct PushKeys { - p256dh: String, - auth: String, +pub struct PushKeys { + pub p256dh: String, + pub auth: String, } /// Subscribe user to push notifications /// Requests notification permission if needed, then subscribes to push pub async fn subscribe_to_push_notifications() { - use gloo_net::http::Request; + use crate::api; // First, request notification permission if not already granted let window = web_sys::window().expect("window should exist"); @@ -347,36 +347,19 @@ pub async fn subscribe_to_push_notifications() { 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, + let public_key = match api::push::get_public_key().await { + Ok(key) => key, Err(e) => { log::error!("Failed to get VAPID public key: {:?}", e); return; } }; - let public_key_data: serde_json::Value = match public_key_response.json().await { - Ok(data) => data, - Err(e) => { - log::error!("Failed to parse VAPID public key: {:?}", e); - return; - } - }; - - let public_key = match public_key_data.get("publicKey").and_then(|v| v.as_str()) { - Some(key) => key, - None => { - log::error!("Missing publicKey in response"); - 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) { + let public_key_array = match url_base64_to_uint8array(&public_key) { Ok(arr) => { log::info!("VAPID key converted to Uint8Array (length: {})", arr.length()); arr @@ -495,23 +478,8 @@ pub async fn subscribe_to_push_notifications() { }; // Send to backend (subscription_data is already the struct we need) - let response = match Request::post("/api/push/subscribe") - .json(&subscription_data) - .expect("serialization should succeed") - .send() - .await - { - Ok(resp) => resp, - Err(e) => { - log::error!("Failed to send subscription to backend: {:?}", e); - return; - } - }; - - if response.ok() { - log::info!("Successfully subscribed to push notifications"); - } else { - log::error!("Backend rejected push subscription: {:?}", response.status()); + if let Err(e) = api::push::subscribe(&subscription_data).await { + log::error!("Failed to send subscription to backend: {:?}", e); } }