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() }