diff options
Diffstat (limited to 'src/routes/auth.rs')
| -rw-r--r-- | src/routes/auth.rs | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..c124e03 --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,513 @@ +// 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<AppState>, + ConnectInfo(addr): ConnectInfo<SocketAddr>, + Json(payload): Json<LoginRequest>, +) -> Result<Json<LoginResponse>, 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<AppState>, + ConnectInfo(addr): ConnectInfo<SocketAddr>, + headers: axum::http::HeaderMap, +) -> Result<Json<serde_json::Value>, 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<AppState>, + ConnectInfo(addr): ConnectInfo<SocketAddr>, + headers: axum::http::HeaderMap, +) -> Result<Json<serde_json::Value>, 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" + }))) + } + } +} |
