// Authentication routes use axum::{ extract::{ConnectInfo, State}, http::StatusCode, Json, }; use chrono::Utc; use std::net::SocketAddr; use tracing::{error, warn}; use crate::logging::AuditLogger; use crate::models::{AuthMethod, LoginRequest, LoginResponse, UserInfo}; use crate::{auth, AppState}; pub async fn login( State(state): State, ConnectInfo(addr): ConnectInfo, Json(payload): Json, ) -> Result, StatusCode> { let timestamp = Utc::now(); let client_ip = addr.ip().to_string(); let request_id = AuditLogger::generate_request_id(); // Log the request if let Err(e) = state .logging .log_request( &request_id, timestamp, &client_ip, None, None, "/auth/login", &serde_json::to_value(&payload).unwrap_or_default(), ) .await { error!("[{}] Failed to log request: {}", request_id, e); } // Validate request based on auth method let (user, role) = match payload.method { AuthMethod::Password => { // Password auth - allowed from any IP if let (Some(username), Some(password)) = (&payload.username, &payload.password) { match auth::password::authenticate_password( state.database.pool(), username, password, ) .await { Ok(Some((user, role))) => (user, role), Ok(None) => { // Log security warning super::log_warning_async( &state.logging, &request_id, &format!( "Failed password authentication for user: {} - Invalid credentials", username ), Some("password_auth"), Some(username), Some(0), ); return Ok(Json(LoginResponse { success: false, token: None, user: None, error: Some("Authentication failed".to_string()), })); } Err(e) => { error!( "[{}] Database error during password authentication: {}", request_id, e ); if let Err(log_err) = state .logging .log_error( &request_id, timestamp, &format!("Password auth error: {}", e), Some("authentication"), payload.username.as_deref(), None, ) .await { error!("[{}] Failed to log error: {}", request_id, log_err); } return Err(StatusCode::INTERNAL_SERVER_ERROR); } } } else { warn!("Password authentication attempted without username or password"); super::log_warning_async( &state.logging, &request_id, "Password authentication attempted without username or password", Some("invalid_request"), None, None, ); return Ok(Json(LoginResponse { success: false, token: None, user: None, error: Some("Authentication failed".to_string()), })); } } AuthMethod::Pin => { // PIN auth - only from whitelisted IPs if !state.config.is_pin_ip_whitelisted(&client_ip) { super::log_warning_async( &state.logging, &request_id, &format!( "PIN authentication attempted from non-whitelisted IP: {}", client_ip ), Some("security_violation"), None, None, ); return Ok(Json(LoginResponse { success: false, token: None, user: None, error: Some("Authentication failed".to_string()), })); } if let (Some(username), Some(pin)) = (&payload.username, &payload.pin) { match auth::pin::authenticate_pin( state.database.pool(), username, pin, &state.config.security, ) .await { Ok(Some((user, role))) => (user, role), Ok(None) => { // Log security warning super::log_warning_async( &state.logging, &request_id, &format!("Failed PIN authentication for user: {} - Invalid PIN or PIN not configured", username), Some("pin_auth"), Some(username), Some(0), ); return Ok(Json(LoginResponse { success: false, token: None, user: None, error: Some("Authentication failed".to_string()), })); } Err(e) => { error!( "[{}] Database error during PIN authentication: {}", request_id, e ); if let Err(log_err) = state .logging .log_error( &request_id, timestamp, &format!("PIN auth error: {}", e), Some("authentication"), payload.username.as_deref(), None, ) .await { error!("[{}] Failed to log error: {}", request_id, log_err); } return Err(StatusCode::INTERNAL_SERVER_ERROR); } } } else { warn!("PIN authentication attempted without username or PIN"); super::log_warning_async( &state.logging, &request_id, "PIN authentication attempted without username or PIN", Some("invalid_request"), None, None, ); return Ok(Json(LoginResponse { success: false, token: None, user: None, error: Some("Authentication failed".to_string()), })); } } AuthMethod::Token => { // Token/RFID auth - only from whitelisted IPs if !state.config.is_string_ip_whitelisted(&client_ip) { super::log_warning_async( &state.logging, &request_id, &format!( "Token authentication attempted from non-whitelisted IP: {}", client_ip ), Some("security_violation"), None, None, ); return Ok(Json(LoginResponse { success: false, token: None, user: None, error: Some("Authentication failed".to_string()), })); } if let Some(login_string) = &payload.login_string { match auth::token::authenticate_token( state.database.pool(), login_string, &state.config.security, ) .await { Ok(Some((user, role))) => (user, role), Ok(None) => { // Log security warning super::log_warning_async( &state.logging, &request_id, &format!( "Failed token authentication for login_string: {} - Invalid token", login_string ), Some("token_auth"), Some(login_string), Some(0), ); return Ok(Json(LoginResponse { success: false, token: None, user: None, error: Some("Authentication failed".to_string()), })); } Err(e) => { error!( "[{}] Database error during token authentication: {}", request_id, e ); if let Err(log_err) = state .logging .log_error( &request_id, timestamp, &format!("Token auth error: {}", e), Some("authentication"), Some(login_string), None, ) .await { error!("[{}] Failed to log error: {}", request_id, log_err); } return Err(StatusCode::INTERNAL_SERVER_ERROR); } } } else { warn!("Token authentication attempted without login_string"); super::log_warning_async( &state.logging, &request_id, "Token authentication attempted without login_string", Some("invalid_request"), None, None, ); return Ok(Json(LoginResponse { success: false, token: None, user: None, error: Some("Authentication failed".to_string()), })); } } }; // Create session token let token = state.session_manager.create_session( user.id, user.username.clone(), role.id, role.name.clone(), role.power, ); // Log successful login super::log_info_async( &state.logging, &request_id, &format!( "Successful login for user: {} ({})", user.username, user.name ), Some("authentication"), Some(&user.username), Some(role.power), ); Ok(Json(LoginResponse { success: true, token: Some(token), user: Some(UserInfo { id: user.id, username: user.username, name: user.name, role: role.name, power: role.power, }), error: None, })) } pub async fn logout( State(state): State, ConnectInfo(addr): ConnectInfo, headers: axum::http::HeaderMap, ) -> Result, StatusCode> { let timestamp = Utc::now(); let client_ip = addr.ip().to_string(); let request_id = AuditLogger::generate_request_id(); // Extract token from Authorization header let token = match headers .get("Authorization") .and_then(|header| header.to_str().ok()) .and_then(|auth_str| { if auth_str.starts_with("Bearer ") { Some(auth_str[7..].to_string()) } else { None } }) { Some(t) => t, None => { return Ok(Json(serde_json::json!({ "success": false, "error": "No authorization token provided" }))); } }; // Get username for logging before removing session let username = state .session_manager .get_session(&token) .map(|s| s.username.clone()); // Log the request if let Err(e) = state .logging .log_request( &request_id, timestamp, &client_ip, username.as_deref(), None, "/auth/logout", &serde_json::json!({"action": "logout"}), ) .await { error!("[{}] Failed to log request: {}", request_id, e); } let removed = state.session_manager.remove_session(&token); if removed { // Log successful logout super::log_info_async( &state.logging, &request_id, &format!( "User {} logged out successfully", username.as_deref().unwrap_or("unknown") ), Some("authentication"), username.as_deref(), None, ); Ok(Json(serde_json::json!({ "success": true, "message": "Logged out successfully" }))) } else { Ok(Json(serde_json::json!({ "success": false, "error": "Invalid or expired token" }))) } } pub async fn status( State(state): State, ConnectInfo(addr): ConnectInfo, headers: axum::http::HeaderMap, ) -> Result, StatusCode> { let timestamp = Utc::now(); let client_ip = addr.ip().to_string(); let request_id = AuditLogger::generate_request_id(); // Extract token from Authorization header let token_opt = headers .get("Authorization") .and_then(|header| header.to_str().ok()) .and_then(|auth_str| { if auth_str.starts_with("Bearer ") { Some(auth_str[7..].to_string()) } else { None } }); let token = match token_opt { Some(t) => t, None => { return Ok(Json(serde_json::json!({ "success": false, "valid": false, "error": "No authorization token provided" }))); } }; // Check session validity match state.session_manager.get_session(&token) { Some(session) => { let now = Utc::now(); let timeout_minutes = state.config.get_session_timeout(session.power); let elapsed = (now - session.last_accessed).num_seconds(); let timeout_seconds = (timeout_minutes * 60) as i64; let remaining_seconds = timeout_seconds - elapsed; // Log the request if let Err(e) = state .logging .log_request( &request_id, timestamp, &client_ip, Some(&session.username), Some(session.power), "/auth/status", &serde_json::json!({"token_provided": true}), ) .await { error!("[{}] Failed to log request: {}", request_id, e); } Ok(Json(serde_json::json!({ "success": true, "valid": true, "user": { "id": session.user_id, "username": session.username, "name": session.username, "role": session.role_name, "power": session.power }, "session": { "created_at": session.created_at.to_rfc3339(), "last_accessed": session.last_accessed.to_rfc3339(), "timeout_minutes": timeout_minutes, "remaining_seconds": remaining_seconds.max(0), "expires_at": (session.last_accessed + chrono::Duration::minutes(timeout_minutes as i64)).to_rfc3339() } }))) } None => { // Log the request for invalid token if let Err(e) = state .logging .log_request( &request_id, timestamp, &client_ip, None, None, "/auth/status", &serde_json::json!({"token_provided": true, "valid": false}), ) .await { error!("[{}] Failed to log request: {}", request_id, e); } Ok(Json(serde_json::json!({ "success": true, "valid": false, "message": "Session expired or invalid" }))) } } }