From bb3ec14a75f645d72f47369139a3620fec999143 Mon Sep 17 00:00:00 2001 From: spinline Date: Sat, 7 Feb 2026 14:58:35 +0300 Subject: [PATCH] Fix compilation errors: Add missing dependencies, fix module visibility, and update Axum middleware types --- Cargo.lock | 5 +- backend/Cargo.toml | 1 + backend/src/handlers/auth.rs | 108 ++++++++++++++++++++++++++++++++++ backend/src/handlers/setup.rs | 20 ++++++- backend/src/main.rs | 15 +++-- 5 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 backend/src/handlers/auth.rs diff --git a/Cargo.lock b/Cargo.lock index 6632663..9a7f79d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,9 +99,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arbitrary" @@ -341,6 +341,7 @@ dependencies = [ name = "backend" version = "0.1.0" dependencies = [ + "anyhow", "axum 0.8.8", "axum-extra", "base64 0.22.1", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 86eeede..08c30a4 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -37,3 +37,4 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } bcrypt = "0.17.0" axum-extra = { version = "0.9", features = ["cookie"] } rand = "0.8" +anyhow = "1.0.101" diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs new file mode 100644 index 0000000..b517ff6 --- /dev/null +++ b/backend/src/handlers/auth.rs @@ -0,0 +1,108 @@ +use crate::{db::Db, 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, +} + +#[derive(Serialize)] +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 { + let user = match state.db.get_user_by_username(&payload.username).await { + Ok(Some(u)) => u, + Ok(None) => return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(), + Err(e) => { + tracing::error!("DB error during login: {}", 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) => { + // Create session + let token: String = (0..32).map(|_| { + use rand::{distributions::Alphanumeric, Rng}; + rand::thread_rng().sample(Alphanumeric) as char + }).collect(); + + // Expires in 30 days + let expires_in = 60 * 60 * 24 * 30; + 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: {}", e); + return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create session").into_response(); + } + + let cookie = Cookie::build(("auth_token", token)) + .path("/") + .http_only(true) + .same_site(SameSite::Strict) + .max_age(Duration::seconds(expires_in)) + .build(); + + (StatusCode::OK, jar.add(cookie), "Login successful").into_response() + } + _ => (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(), + } +} + +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() +} + +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(_)) => return StatusCode::OK.into_response(), + _ => {} // Invalid session + } + } + + StatusCode::UNAUTHORIZED.into_response() +} diff --git a/backend/src/handlers/setup.rs b/backend/src/handlers/setup.rs index 3b10033..1bfffbd 100644 --- a/backend/src/handlers/setup.rs +++ b/backend/src/handlers/setup.rs @@ -1,4 +1,4 @@ -use crate::{db::Db, AppState}; +use crate::AppState; use axum::{ extract::{State, Json}, http::StatusCode, @@ -18,6 +18,13 @@ 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, @@ -29,6 +36,17 @@ pub async fn get_setup_status_handler(State(state): State) -> impl Int Json(SetupStatusResponse { completed }).into_response() } +#[utoipa::path( + post, + path = "/api/setup", + request_body = SetupRequest, + responses( + (status = 200, description = "Setup completed"), + (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, Json(payload): Json, diff --git a/backend/src/main.rs b/backend/src/main.rs index 26d4663..5e2682a 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -15,6 +15,7 @@ use axum::{ extract::Request, response::Response, http::StatusCode, + body::Body, }; use axum_extra::extract::cookie::CookieJar; use clap::Parser; @@ -46,7 +47,7 @@ pub struct AppState { async fn auth_middleware( state: axum::extract::State, jar: CookieJar, - request: Request, + request: Request, next: Next, ) -> Result { // Skip auth for public paths @@ -110,6 +111,8 @@ struct Args { 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 ), @@ -128,7 +131,8 @@ struct Args { push::PushSubscription, push::PushKeys, handlers::auth::LoginRequest, - handlers::setup::SetupRequest + handlers::setup::SetupRequest, + handlers::setup::SetupStatusResponse ) ), tags( @@ -152,6 +156,8 @@ struct ApiDoc; handlers::get_global_limit_handler, handlers::set_global_limit_handler, handlers::auth::login_handler, + handlers::auth::logout_handler, + handlers::auth::check_auth_handler, handlers::setup::setup_handler, handlers::setup::get_setup_status_handler ), @@ -168,7 +174,8 @@ struct ApiDoc; shared::SetLabelRequest, shared::GlobalLimitRequest, handlers::auth::LoginRequest, - handlers::setup::SetupRequest + handlers::setup::SetupRequest, + handlers::setup::SetupStatusResponse ) ), tags( @@ -210,7 +217,7 @@ async fn main() { } } - let db = match db::Db::new(&args.db_url).await { + let db: db::Db = match db::Db::new(&args.db_url).await { Ok(db) => db, Err(e) => { tracing::error!("Failed to connect to database: {}", e);