refactor: move DB to shared crate, convert push endpoints to server functions, remove dead REST handlers
All checks were successful
Build MIPS Binary / build (push) Successful in 5m17s

This commit is contained in:
spinline
2026-02-10 02:05:04 +03:00
parent c2bf6e6fd5
commit 4b3e713657
14 changed files with 61 additions and 96 deletions

View File

@@ -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"

View File

@@ -1,16 +0,0 @@
-- 001_init.sql
-- Initial schema for users and sessions
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);

View File

@@ -1,13 +0,0 @@
-- 002_push_subscriptions.sql
-- Push notification subscriptions storage
CREATE TABLE IF NOT EXISTS push_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
endpoint TEXT NOT NULL UNIQUE,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Index for faster lookups by endpoint
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_endpoint ON push_subscriptions(endpoint);

View File

@@ -1,148 +0,0 @@
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite, Row, sqlite::SqliteConnectOptions};
use std::time::Duration;
use anyhow::Result;
use std::str::FromStr;
#[derive(Clone)]
pub struct Db {
pool: Pool<Sqlite>,
}
impl Db {
pub async fn new(db_url: &str) -> Result<Self> {
let options = SqliteConnectOptions::from_str(db_url)?
.create_if_missing(true)
.busy_timeout(Duration::from_secs(10)) // Bekleme süresini 10 saniyeye çıkardık
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(sqlx::sqlite::SqliteSynchronous::Normal);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.acquire_timeout(Duration::from_secs(10))
.connect_with(options)
.await?;
let db = Self { pool };
db.run_migrations().await?;
Ok(db)
}
async fn run_migrations(&self) -> Result<()> {
sqlx::migrate!("./migrations").run(&self.pool).await?;
Ok(())
}
// --- User Operations ---
pub async fn create_user(&self, username: &str, password_hash: &str) -> Result<()> {
sqlx::query("INSERT INTO users (username, password_hash) VALUES (?, ?)")
.bind(username)
.bind(password_hash)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get_user_by_username(&self, username: &str) -> Result<Option<(i64, String)>> {
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))))
}
pub async fn get_username_by_id(&self, id: i64) -> Result<Option<String>> {
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<bool> {
let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(&self.pool)
.await?;
Ok(row.0 > 0)
}
// --- Session Operations ---
pub async fn create_session(&self, user_id: i64, token: &str, expires_at: i64) -> Result<()> {
sqlx::query("INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, datetime(?, 'unixepoch'))")
.bind(token)
.bind(user_id)
.bind(expires_at)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get_session_user(&self, token: &str) -> Result<Option<i64>> {
let row = sqlx::query("SELECT user_id FROM sessions WHERE token = ? AND expires_at > datetime('now')")
.bind(token)
.fetch_optional(&self.pool)
.await?;
Ok(row.map(|r| r.get(0)))
}
pub async fn delete_session(&self, token: &str) -> Result<()> {
sqlx::query("DELETE FROM sessions WHERE token = ?")
.bind(token)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn update_password(&self, user_id: i64, password_hash: &str) -> Result<()> {
sqlx::query("UPDATE users SET password_hash = ? WHERE id = ?")
.bind(password_hash)
.bind(user_id)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn delete_all_sessions_for_user(&self, user_id: i64) -> Result<()> {
sqlx::query("DELETE FROM sessions WHERE user_id = ?")
.bind(user_id)
.execute(&self.pool)
.await?;
Ok(())
}
// --- Push Subscription Operations ---
pub async fn save_push_subscription(&self, endpoint: &str, p256dh: &str, auth: &str) -> Result<()> {
sqlx::query(
"INSERT INTO push_subscriptions (endpoint, p256dh, auth) VALUES (?, ?, ?)
ON CONFLICT(endpoint) DO UPDATE SET p256dh = EXCLUDED.p256dh, auth = EXCLUDED.auth"
)
.bind(endpoint)
.bind(p256dh)
.bind(auth)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn remove_push_subscription(&self, endpoint: &str) -> Result<()> {
sqlx::query("DELETE FROM push_subscriptions WHERE endpoint = ?")
.bind(endpoint)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get_all_push_subscriptions(&self) -> Result<Vec<(String, String, String)>> {
let rows = sqlx::query_as::<_, (String, String, String)>(
"SELECT endpoint, p256dh, auth FROM push_subscriptions"
)
.fetch_all(&self.pool)
.await?;
Ok(rows)
}
}

View File

@@ -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<crate::AppState>,
) -> 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<crate::AppState>,
axum::extract::Json(subscription): axum::extract::Json<crate::push::PushSubscription>,
) -> impl IntoResponse {
tracing::info!("Received push subscription: {:?}", subscription);
state.push_store.add_subscription(subscription).await;
(StatusCode::OK, "Subscription saved").into_response()
}

View File

@@ -1,4 +1,3 @@
mod db;
mod diff;
mod handlers;
#[cfg(feature = "push-notifications")]
@@ -42,7 +41,7 @@ pub struct AppState {
pub tx: Arc<watch::Sender<Vec<Torrent>>>,
pub event_bus: broadcast::Sender<AppEvent>,
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<tokio::sync::Notify>,
@@ -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<Body>| {
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())

View File

@@ -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 {