diff --git a/Cargo.lock b/Cargo.lock index 5e7aec2..173df8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,7 +330,6 @@ dependencies = [ "serde", "serde_json", "shared", - "sqlx", "thiserror 2.0.18", "time", "tokio", @@ -3651,12 +3650,14 @@ dependencies = [ name = "shared" version = "0.1.0" dependencies = [ + "anyhow", "bytes", "leptos", "leptos_axum", "leptos_router", "quick-xml", "serde", + "sqlx", "thiserror 2.0.18", "tokio", "utoipa", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 82ef086..678e06e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -33,7 +33,6 @@ utoipa-swagger-ui = { version = "9.0", features = ["axum"], optional = true } web-push = { version = "0.10", default-features = false, features = ["hyper-client"], optional = true } base64 = "0.22" openssl = { version = "0.10", features = ["vendored"], optional = true } -sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } bcrypt = "0.17.0" axum-extra = { version = "0.10", features = ["cookie"] } rand = "0.8" diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs index c1c63b7..308df88 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs @@ -49,21 +49,3 @@ pub async fn handle_timeout_error(err: BoxError) -> (StatusCode, &'static str) { ) } } - -#[cfg(feature = "push-notifications")] -pub async fn get_push_public_key_handler( - axum::extract::State(state): axum::extract::State, -) -> impl IntoResponse { - let public_key = state.push_store.get_public_key(); - (StatusCode::OK, axum::extract::Json(serde_json::json!({ "publicKey": public_key }))).into_response() -} - -#[cfg(feature = "push-notifications")] -pub async fn subscribe_push_handler( - axum::extract::State(state): axum::extract::State, - axum::extract::Json(subscription): axum::extract::Json, -) -> impl IntoResponse { - tracing::info!("Received push subscription: {:?}", subscription); - state.push_store.add_subscription(subscription).await; - (StatusCode::OK, "Subscription saved").into_response() -} diff --git a/backend/src/main.rs b/backend/src/main.rs index e11c29b..b091109 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,4 +1,3 @@ -mod db; mod diff; mod handlers; #[cfg(feature = "push-notifications")] @@ -42,7 +41,7 @@ pub struct AppState { pub tx: Arc>>, pub event_bus: broadcast::Sender, pub scgi_socket_path: String, - pub db: db::Db, + pub db: shared::db::Db, #[cfg(feature = "push-notifications")] pub push_store: push::PushSubscriptionStore, pub notify_poll: Arc, @@ -103,46 +102,6 @@ struct Args { } #[cfg(feature = "swagger")] -#[cfg(feature = "push-notifications")] -#[derive(OpenApi)] -#[openapi( - paths( - handlers::get_push_public_key_handler, - handlers::subscribe_push_handler, - handlers::auth::login_handler, - handlers::auth::logout_handler, - handlers::auth::check_auth_handler, - handlers::setup::setup_handler, - handlers::setup::get_setup_status_handler - ), - components( - schemas( - shared::AddTorrentRequest, - shared::TorrentActionRequest, - shared::Torrent, - shared::TorrentStatus, - shared::TorrentFile, - shared::TorrentPeer, - shared::TorrentTracker, - shared::SetFilePriorityRequest, - shared::SetLabelRequest, - shared::GlobalLimitRequest, - push::PushSubscription, - push::PushKeys, - handlers::auth::LoginRequest, - handlers::setup::SetupRequest, - handlers::setup::SetupStatusResponse, - handlers::auth::UserResponse - ) - ), - tags( - (name = "vibetorrent", description = "VibeTorrent API") - ) -)] -struct ApiDoc; - -#[cfg(feature = "swagger")] -#[cfg(not(feature = "push-notifications"))] #[derive(OpenApi)] #[openapi( paths( @@ -206,7 +165,7 @@ async fn main() { } } - let db: db::Db = match db::Db::new(&args.db_url).await { + let db: shared::db::Db = match shared::db::Db::new(&args.db_url).await { Ok(db) => db, Err(e) => { tracing::error!("Failed to connect to database: {}", e); @@ -470,6 +429,7 @@ async fn main() { // Setup & Auth Routes (cookie-based, stay as REST) let scgi_path_for_ctx = args.socket.clone(); + let db_for_ctx = db.clone(); let app = app .route("/api/setup/status", get(handlers::setup::get_setup_status_handler)) .route("/api/setup", post(handlers::setup::setup_handler)) @@ -484,12 +444,18 @@ async fn main() { .route("/api/events", get(sse::sse_handler)) .route("/api/server_fns/{*fn_name}", post({ let scgi_path = scgi_path_for_ctx.clone(); + let db = db_for_ctx.clone(); move |req: Request| { + let scgi_path = scgi_path.clone(); + let db = db.clone(); leptos_axum::handle_server_fns_with_context( move || { leptos::context::provide_context(shared::ServerContext { scgi_socket_path: scgi_path.clone(), }); + leptos::context::provide_context(shared::DbContext { + db: db.clone(), + }); }, req, ) @@ -497,11 +463,6 @@ async fn main() { })) .fallback(handlers::static_handler); - #[cfg(feature = "push-notifications")] - let app = app - .route("/api/push/public-key", get(handlers::get_push_public_key_handler)) - .route("/api/push/subscribe", post(handlers::subscribe_push_handler)); - let app = app .layer(middleware::from_fn_with_state(app_state.clone(), auth_middleware)) .layer(TraceLayer::new_for_http()) diff --git a/backend/src/push.rs b/backend/src/push.rs index 47207ef..098bdf3 100644 --- a/backend/src/push.rs +++ b/backend/src/push.rs @@ -7,7 +7,7 @@ use web_push::{ }; use futures::StreamExt; -use crate::db::Db; +use shared::db::Db; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct PushSubscription { diff --git a/frontend/src/api/mod.rs b/frontend/src/api/mod.rs index cac3a1e..0c38146 100644 --- a/frontend/src/api/mod.rs +++ b/frontend/src/api/mod.rs @@ -142,25 +142,21 @@ pub mod settings { 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() + shared::server_fns::push::get_public_key() .await - .map_err(|_| ApiError::Network)?; - let key = resp.text().await.map_err(|_| ApiError::Network)?; - Ok(key) + .map_err(|e| ApiError::ServerFn(e.to_string())) } - 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 async fn subscribe(endpoint: &str, p256dh: &str, auth: &str) -> Result<(), ApiError> { + shared::server_fns::push::subscribe_push( + endpoint.to_string(), + p256dh.to_string(), + auth.to_string(), + ) + .await + .map_err(|e| ApiError::ServerFn(e.to_string())) } } diff --git a/frontend/src/store.rs b/frontend/src/store.rs index acb6079..95183f5 100644 --- a/frontend/src/store.rs +++ b/frontend/src/store.rs @@ -55,18 +55,6 @@ pub fn get_action_messages(action: &str) -> (&'static str, &'static str) { } } -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PushSubscriptionData { - pub endpoint: String, - pub keys: PushKeys, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PushKeys { - pub p256dh: String, - pub auth: String, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FilterStatus { All, Downloading, Seeding, Completed, Paused, Inactive, Active, Error, diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 5bbc0c3..fd63801 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -11,6 +11,8 @@ ssr = [ "dep:thiserror", "dep:quick-xml", "dep:leptos_axum", + "dep:sqlx", + "dep:anyhow", "leptos/ssr", "leptos_router/ssr", ] @@ -29,4 +31,8 @@ leptos_axum = { version = "0.8.7", optional = true } tokio = { version = "1", features = ["full"], optional = true } bytes = { version = "1", optional = true } thiserror = { version = "2", optional = true } -quick-xml = { version = "0.31", features = ["serde", "serialize"], optional = true } \ No newline at end of file +quick-xml = { version = "0.31", features = ["serde", "serialize"], optional = true } + +# Database +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"], optional = true } +anyhow = { version = "1.0", optional = true } \ No newline at end of file diff --git a/backend/migrations/001_init.sql b/shared/migrations/001_init.sql similarity index 100% rename from backend/migrations/001_init.sql rename to shared/migrations/001_init.sql diff --git a/backend/migrations/002_push_subscriptions.sql b/shared/migrations/002_push_subscriptions.sql similarity index 100% rename from backend/migrations/002_push_subscriptions.sql rename to shared/migrations/002_push_subscriptions.sql diff --git a/backend/src/db.rs b/shared/src/db.rs similarity index 100% rename from backend/src/db.rs rename to shared/src/db.rs diff --git a/shared/src/lib.rs b/shared/src/lib.rs index f493d80..6a8fef6 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -7,6 +7,9 @@ pub mod scgi; #[cfg(feature = "ssr")] pub mod xmlrpc; +#[cfg(feature = "ssr")] +pub mod db; + pub mod server_fns; #[derive(Clone, Debug)] @@ -14,6 +17,12 @@ pub struct ServerContext { pub scgi_socket_path: String, } +#[cfg(feature = "ssr")] +#[derive(Clone)] +pub struct DbContext { + pub db: db::Db, +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] pub struct Torrent { pub hash: String, diff --git a/shared/src/server_fns/mod.rs b/shared/src/server_fns/mod.rs index 65cca90..634b0f0 100644 --- a/shared/src/server_fns/mod.rs +++ b/shared/src/server_fns/mod.rs @@ -1,2 +1,3 @@ pub mod torrent; pub mod settings; +pub mod push; diff --git a/shared/src/server_fns/push.rs b/shared/src/server_fns/push.rs new file mode 100644 index 0000000..72db4a7 --- /dev/null +++ b/shared/src/server_fns/push.rs @@ -0,0 +1,22 @@ +use leptos::prelude::*; + +#[server(GetPushPublicKey, "/api/server_fns")] +pub async fn get_public_key() -> Result { + let key = std::env::var("VAPID_PUBLIC_KEY") + .map_err(|_| ServerFnError::new("VAPID_PUBLIC_KEY not configured"))?; + Ok(key) +} + +#[server(SubscribePush, "/api/server_fns")] +pub async fn subscribe_push( + endpoint: String, + p256dh: String, + auth: String, +) -> Result<(), ServerFnError> { + let db_ctx = expect_context::(); + db_ctx + .db + .save_push_subscription(&endpoint, &p256dh, &auth) + .await + .map_err(|e| ServerFnError::new(format!("Failed to save subscription: {}", e))) +}