feat: add centralized API service layer for frontend
All checks were successful
Build MIPS Binary / build (push) Successful in 5m18s
All checks were successful
Build MIPS Binary / build (push) Successful in 5m18s
- Create frontend/src/api/mod.rs with centralized HTTP client and error handling - Implement api::auth module (login, logout, check_auth, get_user) - Implement api::torrent module (add, action, delete, start, stop, set_label, set_priority) - Implement api::setup module (get_status, setup) - Implement api::settings module (set_global_limits) - Implement api::push module (get_public_key, subscribe) - Update all components to use api service layer instead of direct gloo_net calls - Add thiserror dependency for error handling
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1218,6 +1218,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
"tailwind_fuse",
|
"tailwind_fuse",
|
||||||
|
"thiserror 2.0.18",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ base64 = "0.22.1"
|
|||||||
serde-wasm-bindgen = "0.6.5"
|
serde-wasm-bindgen = "0.6.5"
|
||||||
leptos-use = "0.13"
|
leptos-use = "0.13"
|
||||||
codee = "0.2"
|
codee = "0.2"
|
||||||
|
thiserror = "2.0"
|
||||||
|
|||||||
243
frontend/src/api/mod.rs
Normal file
243
frontend/src/api/mod.rs
Normal file
@@ -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<bool, ApiError> {
|
||||||
|
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<UserResponse, ApiError> {
|
||||||
|
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<SetupStatusResponse, ApiError> {
|
||||||
|
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<String, ApiError> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,40 +3,24 @@ use crate::components::toast::ToastContainer;
|
|||||||
use crate::components::torrent::table::TorrentTable;
|
use crate::components::torrent::table::TorrentTable;
|
||||||
use crate::components::auth::login::Login;
|
use crate::components::auth::login::Login;
|
||||||
use crate::components::auth::setup::Setup;
|
use crate::components::auth::setup::Setup;
|
||||||
|
use crate::api;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct SetupStatus {
|
|
||||||
completed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct UserResponse {
|
|
||||||
username: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
crate::store::provide_torrent_store();
|
crate::store::provide_torrent_store();
|
||||||
|
|
||||||
// Auth State
|
|
||||||
let (is_loading, set_is_loading) = create_signal(true);
|
let (is_loading, set_is_loading) = create_signal(true);
|
||||||
let (is_authenticated, set_is_authenticated) = create_signal(false);
|
let (is_authenticated, set_is_authenticated) = create_signal(false);
|
||||||
|
|
||||||
// Check Auth & Setup Status on load
|
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
logging::log!("App initialization started...");
|
logging::log!("App initialization started...");
|
||||||
|
|
||||||
// 1. Check Setup Status
|
let setup_res = api::setup::get_status().await;
|
||||||
let setup_res = gloo_net::http::Request::get("/api/setup/status").send().await;
|
|
||||||
|
|
||||||
match setup_res {
|
match setup_res {
|
||||||
Ok(resp) => {
|
|
||||||
if resp.ok() {
|
|
||||||
match resp.json::<SetupStatus>().await {
|
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
if !status.completed {
|
if !status.completed {
|
||||||
logging::log!("Setup not completed, redirecting to /setup");
|
logging::log!("Setup not completed, redirecting to /setup");
|
||||||
@@ -46,23 +30,16 @@ pub fn App() -> impl IntoView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => logging::error!("Failed to parse setup status: {}", e),
|
Err(e) => logging::error!("Failed to get setup status: {:?}", e),
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => logging::error!("Network error checking setup status: {}", e),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check Auth Status
|
let auth_res = api::auth::check_auth().await;
|
||||||
let auth_res = gloo_net::http::Request::get("/api/auth/check").send().await;
|
|
||||||
|
|
||||||
match auth_res {
|
match auth_res {
|
||||||
Ok(resp) => {
|
Ok(true) => {
|
||||||
if resp.status() == 200 {
|
|
||||||
logging::log!("Authenticated!");
|
logging::log!("Authenticated!");
|
||||||
|
|
||||||
// Parse user info
|
if let Ok(user_info) = api::auth::get_user().await {
|
||||||
if let Ok(user_info) = resp.json::<UserResponse>().await {
|
|
||||||
if let Some(store) = use_context::<crate::store::TorrentStore>() {
|
if let Some(store) = use_context::<crate::store::TorrentStore>() {
|
||||||
store.user.set(Some(user_info.username));
|
store.user.set(Some(user_info.username));
|
||||||
}
|
}
|
||||||
@@ -70,33 +47,36 @@ pub fn App() -> impl IntoView {
|
|||||||
|
|
||||||
set_is_authenticated.set(true);
|
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();
|
let pathname = window().location().pathname().unwrap_or_default();
|
||||||
if pathname == "/login" || pathname == "/setup" {
|
if pathname == "/login" || pathname == "/setup" {
|
||||||
logging::log!("Already authenticated, redirecting to home");
|
logging::log!("Already authenticated, redirecting to home");
|
||||||
let navigate = use_navigate();
|
let navigate = use_navigate();
|
||||||
navigate("/", Default::default());
|
navigate("/", Default::default());
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
logging::log!("Not authenticated, redirecting to /login");
|
Ok(false) => {
|
||||||
let navigate = use_navigate();
|
logging::log!("Not authenticated");
|
||||||
|
|
||||||
let pathname = window().location().pathname().unwrap_or_default();
|
let pathname = window().location().pathname().unwrap_or_default();
|
||||||
if pathname != "/login" && pathname != "/setup" {
|
if pathname != "/login" && pathname != "/setup" {
|
||||||
|
let navigate = use_navigate();
|
||||||
navigate("/login", Default::default());
|
navigate("/login", Default::default());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging::error!("Auth check failed: {:?}", e);
|
||||||
|
let navigate = use_navigate();
|
||||||
|
navigate("/login", Default::default());
|
||||||
}
|
}
|
||||||
Err(e) => logging::error!("Network error checking auth status: {}", e),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set_is_loading.set(false);
|
set_is_loading.set(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Initialize push notifications (Only if authenticated)
|
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
if is_authenticated.get() {
|
if is_authenticated.get() {
|
||||||
spawn_local(async {
|
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;
|
gloo_timers::future::TimeoutFuture::new(2000).await;
|
||||||
|
|
||||||
if crate::utils::platform::supports_push_notifications() && !crate::utils::platform::is_safari() {
|
if crate::utils::platform::supports_push_notifications() && !crate::utils::platform::is_safari() {
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use serde::Serialize;
|
use crate::api;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct LoginRequest {
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
remember_me: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Login() -> impl IntoView {
|
pub fn Login() -> impl IntoView {
|
||||||
@@ -23,35 +16,31 @@ pub fn Login() -> impl IntoView {
|
|||||||
|
|
||||||
logging::log!("Attempting login for user: {}", username.get());
|
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 {
|
spawn_local(async move {
|
||||||
let req = LoginRequest {
|
match api::auth::login(&username, &password, remember_me).await {
|
||||||
username: username.get(),
|
Ok(_) => {
|
||||||
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...");
|
logging::log!("Login successful, redirecting...");
|
||||||
// Force a full reload to re-run auth checks in App.rs
|
|
||||||
let _ = window().location().set_href("/");
|
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging::error!("Network error: {}", e);
|
logging::error!("Login failed: {:?}", e);
|
||||||
set_error.set(Some("Bağlantı hatası".to_string()));
|
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);
|
set_loading.set(false);
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use serde::Serialize;
|
use crate::api;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SetupRequest {
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Setup() -> impl IntoView {
|
pub fn Setup() -> impl IntoView {
|
||||||
@@ -35,29 +29,18 @@ pub fn Setup() -> impl IntoView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let username = username.get();
|
||||||
|
let password = pass;
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let req = SetupRequest {
|
match api::setup::setup(&username, &password).await {
|
||||||
username: username.get(),
|
Ok(_) => {
|
||||||
password: pass,
|
logging::log!("Setup completed successfully, redirecting...");
|
||||||
};
|
|
||||||
|
|
||||||
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("/");
|
let _ = window().location().set_href("/");
|
||||||
} else {
|
|
||||||
let text = resp.text().await.unwrap_or_default();
|
|
||||||
set_error.set(Some(format!("Hata: {}", text)));
|
|
||||||
}
|
}
|
||||||
}
|
Err(e) => {
|
||||||
Err(_) => {
|
logging::error!("Setup failed: {:?}", e);
|
||||||
set_error.set(Some("Bağlantı hatası".to_string()));
|
set_error.set(Some("Kurulum başarısız oldu".to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set_loading.set(false);
|
set_loading.set(false);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use leptos::wasm_bindgen::JsCast;
|
use leptos::wasm_bindgen::JsCast;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use crate::api;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Sidebar() -> impl IntoView {
|
pub fn Sidebar() -> impl IntoView {
|
||||||
@@ -76,13 +77,9 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
|
|
||||||
let handle_logout = move |_| {
|
let handle_logout = move |_| {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let client = gloo_net::http::Request::post("/api/auth/logout");
|
if api::auth::logout().await.is_ok() {
|
||||||
if let Ok(resp) = client.send().await {
|
|
||||||
if resp.ok() {
|
|
||||||
// Force full reload to clear state
|
|
||||||
let _ = window().location().set_href("/login");
|
let _ = window().location().set_href("/login");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use leptos::*;
|
|||||||
use leptos_use::storage::use_local_storage;
|
use leptos_use::storage::use_local_storage;
|
||||||
use codee::string::FromToStringCodec;
|
use codee::string::FromToStringCodec;
|
||||||
use shared::GlobalLimitRequest;
|
use shared::GlobalLimitRequest;
|
||||||
|
use crate::api;
|
||||||
|
|
||||||
fn format_bytes(bytes: i64) -> String {
|
fn format_bytes(bytes: i64) -> String {
|
||||||
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||||
@@ -60,8 +61,7 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
let limit_type = limit_type.to_string();
|
let limit_type = limit_type.to_string();
|
||||||
logging::log!("Setting {} limit to {}", limit_type, val);
|
logging::log!("Setting {} limit to {}", limit_type, val);
|
||||||
|
|
||||||
spawn_local(async move {
|
let req = if limit_type == "down" {
|
||||||
let req_body = if limit_type == "down" {
|
|
||||||
GlobalLimitRequest {
|
GlobalLimitRequest {
|
||||||
max_download_rate: Some(val),
|
max_download_rate: Some(val),
|
||||||
max_upload_rate: None,
|
max_upload_rate: None,
|
||||||
@@ -73,26 +73,12 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let client =
|
spawn_local(async move {
|
||||||
gloo_net::http::Request::post("/api/settings/global-limits").json(&req_body);
|
if let Err(e) = api::settings::set_global_limits(&req).await {
|
||||||
|
logging::error!("Failed to set limit: {:?}", e);
|
||||||
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 {
|
} else {
|
||||||
logging::log!("Limit set successfully");
|
logging::log!("Limit set successfully");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(e) => logging::error!("Network error setting limit: {}", e),
|
|
||||||
},
|
|
||||||
Err(e) => logging::error!("Failed to create request: {}", e),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos::html::Dialog;
|
use leptos::html::Dialog;
|
||||||
use crate::store::{show_toast_with_signal, TorrentStore};
|
use crate::store::{show_toast_with_signal, TorrentStore};
|
||||||
use shared::{AddTorrentRequest, NotificationLevel};
|
use crate::api;
|
||||||
|
use shared::NotificationLevel;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -17,7 +18,6 @@ pub fn AddTorrentModal(
|
|||||||
let (is_loading, set_loading) = create_signal(false);
|
let (is_loading, set_loading) = create_signal(false);
|
||||||
let (error_msg, set_error_msg) = create_signal(Option::<String>::None);
|
let (error_msg, set_error_msg) = create_signal(Option::<String>::None);
|
||||||
|
|
||||||
// Effect to open the dialog when the component mounts/renders
|
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
if let Some(dialog) = dialog_ref.get() {
|
if let Some(dialog) = dialog_ref.get() {
|
||||||
let _ = dialog.show_modal();
|
let _ = dialog.show_modal();
|
||||||
@@ -35,16 +35,10 @@ pub fn AddTorrentModal(
|
|||||||
set_loading.set(true);
|
set_loading.set(true);
|
||||||
set_error_msg.set(None);
|
set_error_msg.set(None);
|
||||||
|
|
||||||
|
let uri_val = uri_val;
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let req_body = AddTorrentRequest { uri: uri_val };
|
match api::torrent::add(&uri_val).await {
|
||||||
|
Ok(_) => {
|
||||||
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");
|
logging::log!("Torrent added successfully");
|
||||||
show_toast_with_signal(notifications, NotificationLevel::Success, "Torrent eklendi");
|
show_toast_with_signal(notifications, NotificationLevel::Success, "Torrent eklendi");
|
||||||
set_loading.set(false);
|
set_loading.set(false);
|
||||||
@@ -52,27 +46,11 @@ pub fn AddTorrentModal(
|
|||||||
dialog.close();
|
dialog.close();
|
||||||
}
|
}
|
||||||
on_close.call(());
|
on_close.call(());
|
||||||
} else {
|
}
|
||||||
let status = resp.status();
|
Err(e) => {
|
||||||
let text = resp.text().await.unwrap_or_default();
|
logging::error!("Failed to add torrent: {:?}", e);
|
||||||
logging::error!("Failed to add torrent: {} - {}", status, text);
|
|
||||||
show_toast_with_signal(notifications, NotificationLevel::Error, "Torrent eklenemedi");
|
show_toast_with_signal(notifications, NotificationLevel::Error, "Torrent eklenemedi");
|
||||||
set_error_msg.set(Some(format!("Error {}: {}", status, text)));
|
set_error_msg.set(Some(format!("Error: {:?}", e)));
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)));
|
|
||||||
set_loading.set(false);
|
set_loading.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_use::{on_click_outside, use_timeout_fn};
|
use leptos_use::{on_click_outside, use_timeout_fn};
|
||||||
use crate::store::{get_action_messages, show_toast_with_signal};
|
use crate::store::{get_action_messages, show_toast_with_signal};
|
||||||
|
use crate::api;
|
||||||
use shared::NotificationLevel;
|
use shared::NotificationLevel;
|
||||||
|
|
||||||
fn format_bytes(bytes: i64) -> String {
|
fn format_bytes(bytes: i64) -> String {
|
||||||
@@ -201,54 +202,33 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
|
|
||||||
let on_action = move |(action, hash): (String, String)| {
|
let on_action = move |(action, hash): (String, String)| {
|
||||||
logging::log!("TorrentTable Action: {} on {}", action, hash);
|
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, error_msg) = get_action_messages(&action);
|
||||||
let success_msg = success_msg.to_string();
|
let success_msg = success_msg.to_string();
|
||||||
let error_msg = error_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 notifications = store.notifications;
|
||||||
|
|
||||||
|
let hash = hash.clone();
|
||||||
|
let action = action.clone();
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let action_req = if action == "delete_with_data" {
|
let result = match action.as_str() {
|
||||||
"delete_with_data"
|
"delete" => api::torrent::delete(&hash).await,
|
||||||
} else {
|
"delete_with_data" => api::torrent::delete_with_data(&hash).await,
|
||||||
&action
|
"start" => api::torrent::start(&hash).await,
|
||||||
|
"stop" => api::torrent::stop(&hash).await,
|
||||||
|
_ => api::torrent::action(&hash, &action).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
let req_body = shared::TorrentActionRequest {
|
match result {
|
||||||
hash: hash.clone(),
|
Ok(_) => {
|
||||||
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);
|
logging::log!("Action {} executed successfully", action);
|
||||||
show_toast_with_signal(notifications, NotificationLevel::Success, success_msg);
|
show_toast_with_signal(notifications, NotificationLevel::Success, success_msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging::error!("Network error executing action: {}", e);
|
logging::error!("Action failed: {:?}", e);
|
||||||
show_toast_with_signal(notifications, NotificationLevel::Error, format!("{}: Bağlantı hatası", error_msg));
|
show_toast_with_signal(notifications, NotificationLevel::Error, format!("{}: {:?}", error_msg, e));
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
logging::error!("Failed to serialize request: {}", e);
|
|
||||||
show_toast_with_signal(notifications, NotificationLevel::Error, error_msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
mod app;
|
mod app;
|
||||||
// mod models; // Removed
|
|
||||||
mod components;
|
mod components;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
pub mod api;
|
||||||
|
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|||||||
@@ -314,21 +314,21 @@ use serde::{Deserialize, Serialize};
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
struct PushSubscriptionData {
|
pub struct PushSubscriptionData {
|
||||||
endpoint: String,
|
pub endpoint: String,
|
||||||
keys: PushKeys,
|
pub keys: PushKeys,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
struct PushKeys {
|
pub struct PushKeys {
|
||||||
p256dh: String,
|
pub p256dh: String,
|
||||||
auth: String,
|
pub auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe user to push notifications
|
/// Subscribe user to push notifications
|
||||||
/// Requests notification permission if needed, then subscribes to push
|
/// Requests notification permission if needed, then subscribes to push
|
||||||
pub async fn subscribe_to_push_notifications() {
|
pub async fn subscribe_to_push_notifications() {
|
||||||
use gloo_net::http::Request;
|
use crate::api;
|
||||||
|
|
||||||
// First, request notification permission if not already granted
|
// First, request notification permission if not already granted
|
||||||
let window = web_sys::window().expect("window should exist");
|
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...");
|
log::info!("Notification permission granted! Proceeding with push subscription...");
|
||||||
|
|
||||||
|
|
||||||
// Get VAPID public key from backend
|
// Get VAPID public key from backend
|
||||||
let public_key_response = match Request::get("/api/push/public-key").send().await {
|
let public_key = match api::push::get_public_key().await {
|
||||||
Ok(resp) => resp,
|
Ok(key) => key,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get VAPID public key: {:?}", e);
|
log::error!("Failed to get VAPID public key: {:?}", e);
|
||||||
return;
|
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());
|
log::info!("VAPID public key from backend: {} (len: {})", public_key, public_key.len());
|
||||||
|
|
||||||
// Convert VAPID public key to Uint8Array
|
// 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) => {
|
Ok(arr) => {
|
||||||
log::info!("VAPID key converted to Uint8Array (length: {})", arr.length());
|
log::info!("VAPID key converted to Uint8Array (length: {})", arr.length());
|
||||||
arr
|
arr
|
||||||
@@ -495,23 +478,8 @@ pub async fn subscribe_to_push_notifications() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Send to backend (subscription_data is already the struct we need)
|
// Send to backend (subscription_data is already the struct we need)
|
||||||
let response = match Request::post("/api/push/subscribe")
|
if let Err(e) = api::push::subscribe(&subscription_data).await {
|
||||||
.json(&subscription_data)
|
|
||||||
.expect("serialization should succeed")
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(resp) => resp,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to send subscription to backend: {:?}", 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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user