diff --git a/src/api/auth.rs b/src/api/auth.rs index 9d3baa0..0da6058 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -69,9 +69,10 @@ pub async fn login( // Rate limit check (per IP) if let Some(ip_str) = &ctx.ip_address - && let Ok(ip_addr) = ip_str.parse::() { - state.rate_limiter.check(ip_addr)?; - } + && let Ok(ip_addr) = ip_str.parse::() + { + state.rate_limiter.check(ip_addr)?; + } // Look up user — always run comparable work on failure paths (timing). let user_opt = state @@ -103,9 +104,10 @@ pub async fn login( if !is_authed { record_login_failure(&state, body.username.trim(), ip, ctx.user_agent.as_deref()).await; if let Some(ip_str) = &ctx.ip_address - && let Ok(ip_addr) = ip_str.parse::() { - state.rate_limiter.record_failure(ip_addr); - } + && let Ok(ip_addr) = ip_str.parse::() + { + state.rate_limiter.record_failure(ip_addr); + } // Non-enumerating error for both unknown user and bad password. return Err(AppError::InvalidCredentials); } @@ -117,9 +119,10 @@ pub async fn login( // Clear rate limit on success if let Some(ip_str) = &ctx.ip_address - && let Ok(ip_addr) = ip_str.parse::() { - state.rate_limiter.record_success(ip_addr); - } + && let Ok(ip_addr) = ip_str.parse::() + { + state.rate_limiter.record_success(ip_addr); + } // Session fixation mitigation: revoke prior sessions + refresh tokens. let _ = state diff --git a/src/api/sessions.rs b/src/api/sessions.rs index 4b6dddc..4c74cce 100644 --- a/src/api/sessions.rs +++ b/src/api/sessions.rs @@ -104,11 +104,12 @@ pub async fn terminate_session( ) -> Result> { // If the user is trying to terminate the current session, disallow it if let Some(current_id) = auth.session_id.as_deref() - && id == current_id { - return Err(AppError::InvalidInput( - "Cannot terminate current session".into(), - )); - } + && id == current_id + { + return Err(AppError::InvalidInput( + "Cannot terminate current session".into(), + )); + } // Admins can terminate any session, users can only terminate their own let is_admin = state diff --git a/src/api/tenants.rs b/src/api/tenants.rs index f540232..1887858 100644 --- a/src/api/tenants.rs +++ b/src/api/tenants.rs @@ -54,9 +54,10 @@ pub async fn create_tenant( require(&state.provider, &auth.user.id, "roles:manage").await?; if let Some(ref s) = body.slug - && !s.trim().is_empty() { - crate::identity::slug::validate_slug(s)?; - } + && !s.trim().is_empty() + { + crate::identity::slug::validate_slug(s)?; + } let id = uuid::Uuid::new_v4().to_string(); let tenant = state @@ -124,9 +125,10 @@ pub async fn update_tenant( require(&state.provider, &auth.user.id, "roles:manage").await?; if let Some(ref s) = body.slug - && !s.trim().is_empty() { - crate::identity::slug::validate_slug(s)?; - } + && !s.trim().is_empty() + { + crate::identity::slug::validate_slug(s)?; + } state .provider diff --git a/src/api/ui.rs b/src/api/ui.rs index 5f6488b..4e79e4e 100644 --- a/src/api/ui.rs +++ b/src/api/ui.rs @@ -27,14 +27,15 @@ pub fn ui_dist_dir() -> PathBuf { } } if let Ok(exe) = std::env::current_exe() - && let Some(dir) = exe.parent() { - for rel in ["ui/dist", "../ui/dist", "../../ui/dist"] { - let candidate = dir.join(rel); - if candidate.exists() { - return candidate; - } + && let Some(dir) = exe.parent() + { + for rel in ["ui/dist", "../ui/dist", "../../ui/dist"] { + let candidate = dir.join(rel); + if candidate.exists() { + return candidate; } } + } PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui/dist") } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 26f1706..227df7b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -567,9 +567,10 @@ async fn cmd_init( let db_path = std::path::Path::new(&sqlite_path); println!("Creating database directory..."); if let Some(parent) = db_path.parent() - && !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent)?; - } + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent)?; + } // Create state directory if let Ok(home) = std::env::var("HOME") { @@ -664,9 +665,11 @@ async fn run_init_validation(config: &Config, admin_skipped: bool) -> anyhow::Re let db_path = std::path::Path::new(&sqlite_path); let mut dirs_ok = true; if let Some(parent) = db_path.parent() - && !parent.as_os_str().is_empty() && std::fs::create_dir_all(parent).is_err() { - dirs_ok = false; - } + && !parent.as_os_str().is_empty() + && std::fs::create_dir_all(parent).is_err() + { + dirs_ok = false; + } if let Ok(home) = std::env::var("HOME") { let state_dir = std::path::Path::new(&home).join(".local/state/nx9-auth"); if std::fs::create_dir_all(&state_dir).is_err() { @@ -891,9 +894,10 @@ async fn cmd_backup(config: &Config, path: &std::path::Path) -> anyhow::Result<( } if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent)?; - } + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent)?; + } if path.exists() { std::fs::remove_file(path)?; @@ -956,9 +960,10 @@ async fn cmd_restore(config: &Config, path: &std::path::Path) -> anyhow::Result< let sqlite_path = config.database.sqlite_path(); let target_path = std::path::Path::new(&sqlite_path); if let Some(parent) = target_path.parent() - && !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent)?; - } + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent)?; + } std::fs::copy(path, target_path).with_context(|| { format!("failed to restore backup to {}", target_path.display()) })?; diff --git a/src/config/mod.rs b/src/config/mod.rs index b465fc1..deaec5b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -294,12 +294,13 @@ impl Default for ShutdownConfig { fn resolve_home_path(path: &str) -> String { if let Some(stripped) = path.strip_prefix("~/") - && let Ok(home) = std::env::var("HOME") { - return Path::new(&home) - .join(stripped) - .to_string_lossy() - .into_owned(); - } + && let Ok(home) = std::env::var("HOME") + { + return Path::new(&home) + .join(stripped) + .to_string_lossy() + .into_owned(); + } path.to_string() } @@ -312,10 +313,11 @@ impl Config { *path = resolve_home_path(path); } if let Some(ref mut url) = self.database.url - && let Some(stripped) = url.strip_prefix("sqlite://") { - let clean = resolve_home_path(stripped); - *url = format!("sqlite://{clean}"); - } + && let Some(stripped) = url.strip_prefix("sqlite://") + { + let clean = resolve_home_path(stripped); + *url = format!("sqlite://{clean}"); + } } /// Load and parse config from a TOML file. @@ -371,9 +373,10 @@ impl Config { /// Default user configuration path (~/.config/nx9-auth/config.toml) pub fn default_user_config_path() -> Option { if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") - && !xdg.is_empty() { - return Some(PathBuf::from(xdg).join("nx9-auth/config.toml")); - } + && !xdg.is_empty() + { + return Some(PathBuf::from(xdg).join("nx9-auth/config.toml")); + } if let Ok(home) = std::env::var("HOME") { return Some(PathBuf::from(home).join(".config/nx9-auth/config.toml")); } diff --git a/src/db/mod.rs b/src/db/mod.rs index a5ce607..77f9400 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -58,11 +58,12 @@ pub async fn init_provider( DatabaseBackend::Sqlite => { let path = config.database.sqlite_path(); if let Some(parent) = std::path::Path::new(&path).parent() - && !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent).with_context(|| { - format!("failed to create database directory: {}", parent.display()) - })?; - } + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent).with_context(|| { + format!("failed to create database directory: {}", parent.display()) + })?; + } let max_conn = config.database.max_connections.unwrap_or(16); let min_conn = config.database.min_connections.unwrap_or(1); @@ -177,11 +178,12 @@ pub async fn init_provider( #[cfg(feature = "sqlite")] pub async fn create_pool(path: &str) -> Result { if let Some(parent) = std::path::Path::new(path).parent() - && !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent).with_context(|| { - format!("failed to create database directory: {}", parent.display()) - })?; - } + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent).with_context(|| { + format!("failed to create database directory: {}", parent.display()) + })?; + } let url = if path.starts_with("sqlite://") { path.to_string() } else { diff --git a/src/identity/application_members.rs b/src/identity/application_members.rs index ced0a3b..99ebe68 100644 --- a/src/identity/application_members.rs +++ b/src/identity/application_members.rs @@ -222,39 +222,40 @@ pub async fn update( } if let Some(new_enabled) = enabled - && new_enabled != existing.enabled { - let action = if new_enabled { - "application.member_enabled" - } else { - "application.member_disabled" - }; + && new_enabled != existing.enabled + { + let action = if new_enabled { + "application.member_enabled" + } else { + "application.member_disabled" + }; - let metadata = serde_json::json!({ - "application_id": application_id, - "user_id": user_id, - "role": existing.role, - "enabled": new_enabled, - }) - .to_string(); + let metadata = serde_json::json!({ + "application_id": application_id, + "user_id": user_id, + "role": existing.role, + "enabled": new_enabled, + }) + .to_string(); - let audit_event = crate::audit::AuditEvent { - actor_id: audit_actor_id, - target_id: Some(user_id), - action, - resource_type: "application", - resource_id: Some(application_id), - severity: crate::db::models::AuditSeverity::Info, - ip: audit_ip, - ua: audit_ua, - metadata: Some(&metadata), - }; + let audit_event = crate::audit::AuditEvent { + actor_id: audit_actor_id, + target_id: Some(user_id), + action, + resource_type: "application", + resource_id: Some(application_id), + severity: crate::db::models::AuditSeverity::Info, + ip: audit_ip, + ua: audit_ua, + metadata: Some(&metadata), + }; - provider - .application_members() - .set_enabled_with_audit(application_id, user_id, new_enabled, Some(audit_event)) - .await - .map_err(AppError::Database)?; - } + provider + .application_members() + .set_enabled_with_audit(application_id, user_id, new_enabled, Some(audit_event)) + .await + .map_err(AppError::Database)?; + } provider .application_members() diff --git a/src/identity/applications.rs b/src/identity/applications.rs index 887a302..6e72b33 100644 --- a/src/identity/applications.rs +++ b/src/identity/applications.rs @@ -324,9 +324,10 @@ pub async fn update( .find_by_slug(slug) .await .map_err(AppError::Database)? - && (other.entity_id != id || other.entity_type != "application") { - return Err(AppError::Conflict(format!("slug '{slug}' already exists"))); - } + && (other.entity_id != id || other.entity_type != "application") + { + return Err(AppError::Conflict(format!("slug '{slug}' already exists"))); + } let redirect_json = redirect_uris.map(|v| serde_json::to_string(&v).unwrap_or_default()); let scopes_json = scopes.map(|v| serde_json::to_string(&v).unwrap_or_default()); diff --git a/src/identity/roles.rs b/src/identity/roles.rs index 4c9b76d..6ed5905 100644 --- a/src/identity/roles.rs +++ b/src/identity/roles.rs @@ -191,9 +191,10 @@ pub async fn update_role( .find_by_name(name) .await .map_err(AppError::Database)? - && other.id != id { - return Err(AppError::Conflict(format!("role '{name}' already exists"))); - } + && other.id != id + { + return Err(AppError::Conflict(format!("role '{name}' already exists"))); + } provider .roles() diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index a2a6223..771cc2b 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -74,58 +74,56 @@ where // cannot rely solely on the HttpOnly cookie. if let Some(auth_header) = parts.headers.get(axum::http::header::AUTHORIZATION) && let Ok(value) = auth_header.to_str() - && let Some(raw) = value.strip_prefix("Bearer ") { - let raw = raw.trim(); + && let Some(raw) = value.strip_prefix("Bearer ") + { + let raw = raw.trim(); - // 2a. Personal access token - if let Some(token) = tokens::validate_token(&app_state.provider, raw).await? { - let user = app_state - .provider - .users() - .find_by_id(&token.user_id) - .await - .map_err(AppError::Database)? - .ok_or(AppError::Unauthorized)?; + // 2a. Personal access token + if let Some(token) = tokens::validate_token(&app_state.provider, raw).await? { + let user = app_state + .provider + .users() + .find_by_id(&token.user_id) + .await + .map_err(AppError::Database)? + .ok_or(AppError::Unauthorized)?; - if !user.is_active() { - return Err(AppError::Unauthorized); - } - - return Ok(AuthUser { - user, - method: AuthMethod::Token, - session_id: None, - }); - } - - // 2b. Session token (same value as nx9_session cookie) - if let Some(session) = sessions::validate_session( - &app_state.provider, - raw, - &app_state.config.security, - ) - .await? - { - let user = app_state - .provider - .users() - .find_by_id(&session.user_id) - .await - .map_err(AppError::Database)? - .ok_or(AppError::Unauthorized)?; - - if !user.is_active() { - return Err(AppError::Unauthorized); - } - - return Ok(AuthUser { - user, - method: AuthMethod::Session, - session_id: Some(session.id), - }); - } + if !user.is_active() { + return Err(AppError::Unauthorized); } + return Ok(AuthUser { + user, + method: AuthMethod::Token, + session_id: None, + }); + } + + // 2b. Session token (same value as nx9_session cookie) + if let Some(session) = + sessions::validate_session(&app_state.provider, raw, &app_state.config.security) + .await? + { + let user = app_state + .provider + .users() + .find_by_id(&session.user_id) + .await + .map_err(AppError::Database)? + .ok_or(AppError::Unauthorized)?; + + if !user.is_active() { + return Err(AppError::Unauthorized); + } + + return Ok(AuthUser { + user, + method: AuthMethod::Session, + session_id: Some(session.id), + }); + } + } + Err(AppError::Unauthorized) } } diff --git a/src/runtime/application.rs b/src/runtime/application.rs index 7ffe5fb..aa052f4 100644 --- a/src/runtime/application.rs +++ b/src/runtime/application.rs @@ -163,11 +163,12 @@ impl Lifecycle for Application { } if self.router.is_none() - && let Some(provider) = &self.provider { - let app_state = crate::state::AppState::new(provider.clone(), config); - let router = crate::api::router::build(app_state); - self.router = Some(router); - } + && let Some(provider) = &self.provider + { + let app_state = crate::state::AppState::new(provider.clone(), config); + let router = crate::api::router::build(app_state); + self.router = Some(router); + } Ok(()) } diff --git a/src/security/passwords.rs b/src/security/passwords.rs index 2f0f97e..3baa9da 100644 --- a/src/security/passwords.rs +++ b/src/security/passwords.rs @@ -75,10 +75,22 @@ pub fn validate_password_strength(password: &str, is_admin: bool) -> Result<(), // 1. Exact matches for highly common passwords let exact_weak: HashSet<&str> = [ - "password", "admin123", "qwerty", "12345678", "123456789", - "administrator", "nx9-auth", "nx9auth", "password123", "admin", - "letmein", "welcome", "password12345" - ].into_iter().collect(); + "password", + "admin123", + "qwerty", + "12345678", + "123456789", + "administrator", + "nx9-auth", + "nx9auth", + "password123", + "admin", + "letmein", + "welcome", + "password12345", + ] + .into_iter() + .collect(); if exact_weak.contains(normalized.as_str()) { return Err(AppError::InvalidInput( @@ -98,11 +110,14 @@ pub fn validate_password_strength(password: &str, is_admin: bool) -> Result<(), } // 3. Reject single repeated characters - if password.chars().all(|c| c == password.chars().next().unwrap()) { + if password + .chars() + .all(|c| c == password.chars().next().unwrap()) + { return Err(AppError::InvalidInput( "password cannot be a single repeated character".to_string(), )); } Ok(()) -} \ No newline at end of file +} diff --git a/src/security/rate_limit.rs b/src/security/rate_limit.rs index c761227..81e9045 100644 --- a/src/security/rate_limit.rs +++ b/src/security/rate_limit.rs @@ -73,9 +73,10 @@ impl RateLimiter { pub fn check(&self, ip: IpAddr) -> Result<(), AppError> { if let Some(s) = self.state.get(&ip) && let Some(until) = s.locked_until - && Instant::now() < until { - return Err(AppError::RateLimited); - } + && Instant::now() < until + { + return Err(AppError::RateLimited); + } Ok(()) } @@ -86,9 +87,10 @@ impl RateLimiter { // Clear the lockout if it has expired if let Some(until) = s.locked_until - && now >= until { - s.locked_until = None; - } + && now >= until + { + s.locked_until = None; + } // Prune old failures outside the window let cutoff = now - self.window; diff --git a/src/security/sessions.rs b/src/security/sessions.rs index d55e24d..f9a9c67 100644 --- a/src/security/sessions.rs +++ b/src/security/sessions.rs @@ -78,14 +78,15 @@ pub async fn validate_session( // Check absolute expiry if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(&session.expires_at) - && now > expires { - provider - .sessions() - .revoke(&session.id) - .await - .map_err(AppError::Database)?; - return Ok(None); - } + && now > expires + { + provider + .sessions() + .revoke(&session.id) + .await + .map_err(AppError::Database)?; + return Ok(None); + } // Check idle timeout if let Ok(last_seen) = chrono::DateTime::parse_from_rfc3339(&session.last_seen_at) { diff --git a/src/security/tokens.rs b/src/security/tokens.rs index 740ecd0..5dcfec0 100644 --- a/src/security/tokens.rs +++ b/src/security/tokens.rs @@ -132,9 +132,10 @@ pub async fn validate_token( // Check expiry if set if let Some(ref exp) = token.expires_at && let Ok(expires) = chrono::DateTime::parse_from_rfc3339(exp) - && chrono::Utc::now() > expires { - return Ok(None); - } + && chrono::Utc::now() > expires + { + return Ok(None); + } // Touch last_used_at (fire-and-forget) let _ = provider.tokens().update_last_used(&token.id).await;