aboutsummaryrefslogtreecommitdiff
path: root/src/routes/auth.rs
diff options
context:
space:
mode:
authorUMTS at Teleco <crt@teleco.ch>2025-12-13 02:48:13 +0100
committerUMTS at Teleco <crt@teleco.ch>2025-12-13 02:48:13 +0100
commite52b8e1c2e110d0feb74feb7905c2ff064b51d55 (patch)
tree3090814e422250e07e72cf1c83241ffd95cf20f7 /src/routes/auth.rs
committing to insanityHEADmaster
Diffstat (limited to 'src/routes/auth.rs')
-rw-r--r--src/routes/auth.rs513
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"
+ })))
+ }
+ }
+}