//! Authentication module //! //! Handles JWT validation for dashboard users and API key //! validation for agents. pub mod jwt; pub mod password; pub use jwt::{Claims, JwtConfig}; pub use password::{hash_password, verify_password, generate_random_password}; use axum::{ extract::FromRequestParts, http::{request::Parts, StatusCode}, }; use std::sync::Arc; /// Authenticated user from JWT #[derive(Debug, Clone)] pub struct AuthenticatedUser { pub user_id: String, pub username: String, pub role: String, pub permissions: Vec, } impl AuthenticatedUser { /// Check if user has a specific permission pub fn has_permission(&self, permission: &str) -> bool { if self.role == "admin" { return true; } self.permissions.contains(&permission.to_string()) } /// Check if user is admin pub fn is_admin(&self) -> bool { self.role == "admin" } } impl From for AuthenticatedUser { fn from(claims: Claims) -> Self { Self { user_id: claims.sub, username: claims.username, role: claims.role, permissions: claims.permissions, } } } /// Authenticated agent from API key #[derive(Debug, Clone)] pub struct AuthenticatedAgent { pub agent_id: String, pub org_id: String, } /// JWT configuration stored in app state #[derive(Clone)] pub struct AuthState { pub jwt_config: Arc, } impl AuthState { pub fn new(jwt_secret: String, expiry_hours: i64) -> Self { Self { jwt_config: Arc::new(JwtConfig::new(jwt_secret, expiry_hours)), } } } /// Extract authenticated user from request #[axum::async_trait] impl FromRequestParts for AuthenticatedUser where S: Send + Sync, { type Rejection = (StatusCode, &'static str); async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { // Get Authorization header let auth_header = parts .headers .get("Authorization") .and_then(|v| v.to_str().ok()) .ok_or((StatusCode::UNAUTHORIZED, "Missing Authorization header"))?; // Extract Bearer token let token = auth_header .strip_prefix("Bearer ") .ok_or((StatusCode::UNAUTHORIZED, "Invalid Authorization format"))?; // Get JWT config from extensions (set by middleware) let jwt_config = parts .extensions .get::>() .ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Auth not configured"))?; // Validate token let claims = jwt_config .validate_token(token) .map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid or expired token"))?; Ok(AuthenticatedUser::from(claims)) } } /// Optional authenticated user (doesn't reject if not authenticated) #[derive(Debug, Clone)] pub struct OptionalUser(pub Option); #[axum::async_trait] impl FromRequestParts for OptionalUser where S: Send + Sync, { type Rejection = (StatusCode, &'static str); async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { match AuthenticatedUser::from_request_parts(parts, state).await { Ok(user) => Ok(OptionalUser(Some(user))), Err(_) => Ok(OptionalUser(None)), } } } /// Require admin role #[derive(Debug, Clone)] pub struct AdminUser(pub AuthenticatedUser); #[axum::async_trait] impl FromRequestParts for AdminUser where S: Send + Sync, { type Rejection = (StatusCode, &'static str); async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let user = AuthenticatedUser::from_request_parts(parts, state).await?; if user.is_admin() { Ok(AdminUser(user)) } else { Err((StatusCode::FORBIDDEN, "Admin access required")) } } } /// Validate an agent API key (placeholder for MVP) pub fn validate_agent_key(_api_key: &str) -> Option { // TODO: Implement actual API key validation against database // For now, accept any key for agent connections Some(AuthenticatedAgent { agent_id: "mvp-agent".to_string(), org_id: "mvp-org".to_string(), }) }