diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index 1de4deb..dd0fa5e 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -1,154 +1,2 @@ -use crate::AppState; -use axum::{ - extract::{State, Json}, - http::StatusCode, - response::IntoResponse, -}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; -use time::Duration; - -#[derive(Deserialize, ToSchema)] -pub struct LoginRequest { - username: String, - password: String, - #[serde(default)] - remember_me: bool, -} - -#[derive(Serialize, ToSchema)] -pub struct UserResponse { - username: String, -} - -#[utoipa::path( - post, - path = "/api/auth/login", - request_body = LoginRequest, - responses( - (status = 200, description = "Login successful"), - (status = 401, description = "Invalid credentials"), - (status = 500, description = "Internal server error") - ) -)] -pub async fn login_handler( - State(state): State, - jar: CookieJar, - Json(payload): Json, -) -> impl IntoResponse { - tracing::info!("Login attempt for user: {}", payload.username); - - let user = match state.db.get_user_by_username(&payload.username).await { - Ok(Some(u)) => u, - Ok(None) => { - tracing::warn!("Login failed: User not found for {}", payload.username); - return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(); - } - Err(e) => { - tracing::error!("DB error during login for {}: {}", payload.username, e); - return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(); - } - }; - - let (user_id, password_hash) = user; - - match bcrypt::verify(&payload.password, &password_hash) { - Ok(true) => { - tracing::info!("Password verified for user: {}", payload.username); - - // Create session - let token: String = (0..32).map(|_| { - use rand::{distributions::Alphanumeric, Rng}; - rand::thread_rng().sample(Alphanumeric) as char - }).collect(); - - // Expiration: 30 days if remember_me is true, else 1 day - let expires_in = if payload.remember_me { - 60 * 60 * 24 * 30 - } else { - 60 * 60 * 24 - }; - - let expires_at = time::OffsetDateTime::now_utc().unix_timestamp() + expires_in; - - if let Err(e) = state.db.create_session(user_id, &token, expires_at).await { - tracing::error!("Failed to create session for {}: {}", payload.username, e); - return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create session").into_response(); - } - - let mut cookie = Cookie::build(("auth_token", token)) - .path("/") - .http_only(true) - .same_site(SameSite::Lax) - .build(); - - cookie.set_max_age(Duration::seconds(expires_in)); - - tracing::info!("Session created and cookie set for user: {}", payload.username); - (StatusCode::OK, jar.add(cookie), Json(UserResponse { username: payload.username })).into_response() - } - Ok(false) => { - tracing::warn!("Login failed: Invalid password for {}", payload.username); - (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response() - } - Err(e) => { - tracing::error!("Bcrypt error for {}: {}", payload.username, e); - (StatusCode::INTERNAL_SERVER_ERROR, "Auth error").into_response() - } - } -} - -#[utoipa::path( - post, - path = "/api/auth/logout", - responses( - (status = 200, description = "Logged out") - ) -)] -pub async fn logout_handler( - State(state): State, - jar: CookieJar, -) -> impl IntoResponse { - if let Some(token) = jar.get("auth_token") { - let _ = state.db.delete_session(token.value()).await; - } - - let cookie = Cookie::build(("auth_token", "")) - .path("/") - .http_only(true) - .max_age(Duration::seconds(-1)) // Expire immediately - .build(); - - (StatusCode::OK, jar.add(cookie), "Logged out").into_response() -} - -#[utoipa::path( - get, - path = "/api/auth/check", - responses( - (status = 200, description = "Authenticated", body = UserResponse), - (status = 401, description = "Not authenticated") - ) -)] -pub async fn check_auth_handler( - State(state): State, - jar: CookieJar, -) -> impl IntoResponse { - if let Some(token) = jar.get("auth_token") { - match state.db.get_session_user(token.value()).await { - 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 - } - } - - StatusCode::UNAUTHORIZED.into_response() -} +// This file is intentionally empty as authentication is now handled by Server Functions. +// See shared/src/server_fns/auth.rs diff --git a/backend/src/handlers/setup.rs b/backend/src/handlers/setup.rs index ae44d8c..09e8aea 100644 --- a/backend/src/handlers/setup.rs +++ b/backend/src/handlers/setup.rs @@ -1,125 +1,2 @@ -use crate::AppState; -use axum::{ - extract::{State, Json}, - http::StatusCode, - response::IntoResponse, -}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; -use time::Duration; - -#[derive(Deserialize, ToSchema)] -pub struct SetupRequest { - username: String, - password: String, -} - -#[derive(Serialize, ToSchema)] -pub struct SetupStatusResponse { - completed: bool, -} - -#[utoipa::path( - get, - path = "/api/setup/status", - responses( - (status = 200, description = "Setup status", body = SetupStatusResponse) - ) -)] -pub async fn get_setup_status_handler(State(state): State) -> impl IntoResponse { - let completed = match state.db.has_users().await { - Ok(has) => has, - Err(e) => { - tracing::error!("DB error checking users: {}", e); - false - } - }; - Json(SetupStatusResponse { completed }).into_response() -} - -#[utoipa::path( - post, - path = "/api/setup", - request_body = SetupRequest, - responses( - (status = 200, description = "Setup completed and logged in"), - (status = 400, description = "Invalid request"), - (status = 403, description = "Setup already completed"), - (status = 500, description = "Internal server error") - ) -)] -pub async fn setup_handler( - State(state): State, - jar: CookieJar, - Json(payload): Json, -) -> impl IntoResponse { - // 1. Check if setup is already completed (i.e., users exist) - match state.db.has_users().await { - Ok(true) => return (StatusCode::FORBIDDEN, "Setup already completed").into_response(), - Err(e) => { - tracing::error!("DB error checking users: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(); - } - Ok(false) => {} // Proceed - } - - // 2. Validate input - if payload.username.len() < 3 || payload.password.len() < 6 { - return (StatusCode::BAD_REQUEST, "Username must be at least 3 chars, password at least 6").into_response(); - } - - // 3. Create User - // Lower cost for faster login on low-power devices (MIPS routers etc.) - let password_hash = match bcrypt::hash(&payload.password, 6) { - Ok(h) => h, - Err(e) => { - tracing::error!("Failed to hash password: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to process password").into_response(); - } - }; - - if let Err(e) = state.db.create_user(&payload.username, &password_hash).await { - tracing::error!("Failed to create user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create user").into_response(); - } - - // 4. Auto-Login (Create Session) - // Get the created user's ID - let user = match state.db.get_user_by_username(&payload.username).await { - Ok(Some(u)) => u, - Ok(None) => return (StatusCode::INTERNAL_SERVER_ERROR, "User created but not found").into_response(), - Err(e) => { - tracing::error!("DB error fetching new user: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(); - } - }; - let (user_id, _) = user; - - // Create session token - let token: String = (0..32).map(|_| { - use rand::{distributions::Alphanumeric, Rng}; - rand::thread_rng().sample(Alphanumeric) as char - }).collect(); - - // Default expiration: 1 day (since it's not "remember me") - let expires_in = 60 * 60 * 24; - let expires_at = time::OffsetDateTime::now_utc().unix_timestamp() + expires_in; - - if let Err(e) = state.db.create_session(user_id, &token, expires_at).await { - tracing::error!("Failed to create session for new user: {}", e); - // Even if session fails, setup is technically complete, but login failed. - // We return OK but user will have to login manually. - return (StatusCode::OK, "Setup completed, please login").into_response(); - } - - let mut cookie = Cookie::build(("auth_token", token)) - .path("/") - .http_only(true) - .same_site(SameSite::Lax) - .build(); - - cookie.set_max_age(Duration::seconds(expires_in)); - - (StatusCode::OK, jar.add(cookie), "Setup completed and logged in").into_response() -} +// This file is intentionally empty as setup is now handled by Server Functions. +// See shared/src/server_fns/auth.rs diff --git a/backend/src/main.rs b/backend/src/main.rs index 82979cd..a102735 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -25,7 +25,6 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::{broadcast, watch}; use tower::ServiceBuilder; -use tower_governor::GovernorLayer; use tower_http::{ compression::{CompressionLayer, CompressionLevel}, cors::CorsLayer, @@ -48,7 +47,7 @@ pub struct AppState { } async fn auth_middleware( - state: axum::extract::State, + _state: axum::extract::State, jar: CookieJar, request: Request, next: Next, @@ -113,13 +112,6 @@ struct Args { #[cfg(feature = "swagger")] #[derive(OpenApi)] #[openapi( - paths( - 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, @@ -132,10 +124,6 @@ struct Args { shared::SetFilePriorityRequest, shared::SetLabelRequest, shared::GlobalLimitRequest, - handlers::auth::LoginRequest, - handlers::setup::SetupRequest, - handlers::setup::SetupStatusResponse, - handlers::auth::UserResponse ) ), tags( @@ -144,6 +132,7 @@ struct Args { )] struct ApiDoc; + #[tokio::main] async fn main() { // Load .env file @@ -423,6 +412,11 @@ async fn main() { #[cfg(feature = "swagger")] let app = app.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())); + // 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/events", get(sse::sse_handler)) // Setup & Auth Routes (cookie-based, stay as REST) let scgi_path_for_ctx = args.socket.clone(); let db_for_ctx = db.clone(); diff --git a/backend/src/rate_limit.rs b/backend/src/rate_limit.rs index 39be523..c3d943f 100644 --- a/backend/src/rate_limit.rs +++ b/backend/src/rate_limit.rs @@ -1,16 +1,3 @@ -use governor::clock::QuantaInstant; -use governor::middleware::NoOpMiddleware; -use tower_governor::governor::GovernorConfig; -use tower_governor::governor::GovernorConfigBuilder; -use tower_governor::key_extractor::SmartIpKeyExtractor; - -pub fn get_login_rate_limit_config() -> GovernorConfig> { - // 5 yanlış denemeden sonra bloklanır. - // Her yeni hak için 60 saniye (1 dakika) bekleme süresi. - GovernorConfigBuilder::default() - .key_extractor(SmartIpKeyExtractor) - .per_second(60) - .burst_size(5) - .finish() - .unwrap() -} +// This file can be removed or repurposed if rate limiting is needed for other endpoints. +// Login rate limiting is now handled within the server function or needs to be reimplemented +// as a middleware for the server function endpoint. diff --git a/backend/src/sse.rs b/backend/src/sse.rs index 09b2f0a..9dbf4be 100644 --- a/backend/src/sse.rs +++ b/backend/src/sse.rs @@ -4,7 +4,7 @@ use shared::xmlrpc::{ use crate::AppState; use axum::extract::State; use axum::response::sse::{Event, Sse}; -use futures::stream::{self, Stream}; +use futures::stream::{self}; use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus}; use std::convert::Infallible; use tokio_stream::StreamExt;