use crate::{config::AuthFrameworkSettings, errors::Result};
use chrono;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct AdminSessionRecord {
pub username: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub expires_at: chrono::DateTime<chrono::Utc>,
pub last_activity: chrono::DateTime<chrono::Utc>,
pub csrf_token: String,
}
#[derive(Debug, Clone)]
pub struct AdminLoginAttemptRecord {
pub failed_attempts: u32,
pub first_failed_at: chrono::DateTime<chrono::Utc>,
pub last_failed_at: chrono::DateTime<chrono::Utc>,
pub locked_until: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Clone)]
pub struct AppState {
pub config: Arc<RwLock<AuthFrameworkSettings>>,
pub config_manager: crate::config::ConfigManager,
pub health_status: HealthStatus,
pub server_status: Arc<RwLock<ServerStatus>>,
pub admin_sessions:
Arc<std::sync::Mutex<std::collections::HashMap<String, AdminSessionRecord>>>,
pub admin_login_attempts:
Arc<std::sync::Mutex<std::collections::HashMap<String, AdminLoginAttemptRecord>>>,
pub auth_framework: Option<Arc<crate::AuthFramework>>,
}
impl std::fmt::Debug for AppState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppState")
.field("health_status", &self.health_status)
.field("auth_framework_present", &self.auth_framework.is_some())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerRunState {
Running,
Stopped,
Paused,
}
#[derive(Debug, Clone)]
pub struct ServerStatus {
pub web_server_state: ServerRunState,
pub web_server_port: Option<u16>,
pub last_config_update: Option<chrono::DateTime<chrono::Utc>>,
pub active_sessions: u32,
pub health_status: HealthStatus,
pub started_at: chrono::DateTime<chrono::Utc>,
}
pub(crate) fn format_uptime_since(
started_at: chrono::DateTime<chrono::Utc>,
now: chrono::DateTime<chrono::Utc>,
) -> String {
let elapsed = now.signed_duration_since(started_at);
if elapsed < chrono::Duration::zero() {
return "0m".to_string();
}
let seconds = elapsed.num_seconds() as u64;
let days = seconds / 86_400;
let hours = (seconds % 86_400) / 3_600;
let minutes = (seconds % 3_600) / 60;
if days > 0 {
format!("{days}d {hours}h {minutes}m")
} else if hours > 0 {
format!("{hours}h {minutes}m")
} else {
format!("{minutes}m")
}
}
#[derive(Debug, Clone)]
pub enum HealthStatus {
Healthy,
Warning(String),
Critical(String),
}
#[derive(Debug, Clone)]
pub struct ServerInfo {
pub version: String,
pub uptime: String,
pub status: String,
pub port: Option<u16>,
pub active_sessions: u32,
}
#[derive(Debug, Clone)]
pub struct UserStatistics {
pub total_users: u32,
pub active_sessions: u32,
pub failed_logins_today: u32,
pub new_registrations_today: u32,
}
#[derive(Debug, Clone)]
pub struct SecurityEvent {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub event_type: String,
pub description: String,
pub ip_address: Option<String>,
pub user_id: Option<String>,
}
impl AppState {
pub fn new(settings: AuthFrameworkSettings) -> Result<Self> {
let config = Arc::new(RwLock::new(settings));
let config_manager = crate::config::ConfigManager::new()?;
let server_status = ServerStatus {
web_server_state: ServerRunState::Stopped,
web_server_port: None,
last_config_update: None,
active_sessions: 0,
health_status: HealthStatus::Healthy,
started_at: chrono::Utc::now(),
};
Ok(Self {
config,
config_manager,
health_status: HealthStatus::Healthy,
server_status: Arc::new(RwLock::new(server_status)),
admin_sessions: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
admin_login_attempts: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
auth_framework: None,
})
}
pub fn with_auth_framework(mut self, af: Arc<crate::AuthFramework>) -> Self {
self.auth_framework = Some(af);
self
}
pub async fn get_health_status(&self) -> HealthStatus {
if let Some(ref af) = self.auth_framework {
let storage = af.storage();
match storage.get_kv("health_check_ping").await {
Ok(_) => {
let status = self.server_status.read().await;
match status.web_server_state {
ServerRunState::Running => HealthStatus::Healthy,
_ => HealthStatus::Warning("Web server not running".to_string()),
}
}
Err(e) => HealthStatus::Critical(format!("Storage unavailable: {}", e)),
}
} else {
HealthStatus::Warning("AuthFramework not attached".to_string())
}
}
pub async fn reload_config(&self) -> Result<()> {
let new_settings = self.config_manager.get_auth_settings()?;
{
let mut config = self.config.write().await;
*config = new_settings;
}
let mut status = self.server_status.write().await;
status.last_config_update = Some(chrono::Utc::now());
Ok(())
}
pub async fn update_server_status(&self, state: ServerRunState, port: Option<u16>) {
let mut status = self.server_status.write().await;
status.web_server_state = state;
status.web_server_port = port;
}
pub async fn get_server_info(&self) -> Result<ServerInfo> {
let status = self.server_status.read().await;
let uptime = format_uptime_since(status.started_at, chrono::Utc::now());
Ok(ServerInfo {
version: env!("CARGO_PKG_VERSION").to_string(),
uptime,
status: match status.web_server_state {
ServerRunState::Running => "Running",
ServerRunState::Stopped => "Stopped",
ServerRunState::Paused => "Paused",
}
.to_string(),
port: status.web_server_port,
active_sessions: status.active_sessions,
})
}
pub async fn get_user_statistics(&self) -> Result<UserStatistics> {
if let Some(ref af) = self.auth_framework {
let storage = af.storage();
let total_users = match storage.get_kv("users:index").await {
Ok(Some(bytes)) => serde_json::from_slice::<Vec<String>>(&bytes)
.map(|v| v.len() as u32)
.unwrap_or(0),
_ => 0,
};
let status = self.server_status.read().await;
let (failed_logins, new_regs) = match af.get_security_audit_stats().await {
Ok(stats) => (
stats.failed_logins_24h as u32,
stats.password_resets_24h as u32,
),
Err(_) => (0, 0),
};
Ok(UserStatistics {
total_users,
active_sessions: status.active_sessions,
failed_logins_today: failed_logins,
new_registrations_today: new_regs,
})
} else {
let status = self.server_status.read().await;
Ok(UserStatistics {
total_users: 0,
active_sessions: status.active_sessions,
failed_logins_today: 0,
new_registrations_today: 0,
})
}
}
pub async fn get_recent_security_events(&self) -> Result<Vec<SecurityEvent>> {
if let Some(ref af) = self.auth_framework {
let logs = af
.get_permission_audit_logs(None, None, None, Some(20))
.await?;
let events = logs
.into_iter()
.map(|log_line| {
let (ts, rest) = log_line
.strip_prefix('[')
.and_then(|s| s.split_once("] "))
.unwrap_or(("", &log_line));
let event_type = rest.split_whitespace().next().unwrap_or("Unknown");
let user_id = rest
.split("user=")
.nth(1)
.and_then(|s| s.split_whitespace().next())
.map(|s| s.to_string());
let timestamp = chrono::DateTime::parse_from_rfc3339(ts)
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now());
SecurityEvent {
timestamp,
event_type: event_type.to_string(),
description: rest.to_string(),
ip_address: None,
user_id,
}
})
.collect();
Ok(events)
} else {
Ok(vec![])
}
}
} #[cfg(feature = "cli")]
pub mod cli;
#[cfg(feature = "tui")]
pub mod tui;
#[cfg(feature = "web-gui")]
pub mod web;
#[derive(Debug, Clone, clap::Subcommand)]
pub enum CliCommand {
Config {
#[command(subcommand)]
action: ConfigAction,
},
Users {
#[command(subcommand)]
action: UserAction,
},
Server {
#[command(subcommand)]
action: ServerAction,
},
Security {
#[command(subcommand)]
action: SecurityAction,
},
Maintenance {
#[command(subcommand)]
action: MaintenanceAction,
},
Status {
#[arg(long)]
detailed: bool,
#[arg(long, default_value = "table")]
format: String,
},
}
#[derive(Debug, Clone, clap::Subcommand)]
pub enum MaintenanceAction {
Backup {
output_path: String,
#[arg(long)]
dry_run: bool,
},
Restore {
backup_path: String,
#[arg(long)]
confirm: bool,
#[arg(long)]
dry_run: bool,
},
Reset {
#[arg(long)]
confirm: bool,
#[arg(long)]
dry_run: bool,
},
CreateMigration {
name: String,
},
}
#[derive(Debug, Clone, clap::Subcommand)]
pub enum ConfigAction {
Show {
section: Option<String>,
#[arg(long, default_value = "table")]
format: String,
},
Set {
key: String,
value: String,
#[arg(long)]
hot_reload: bool,
},
Reset,
Validate {
file: Option<String>,
},
Get {
key: String,
},
Reload {
#[arg(long)]
show_diff: bool,
},
Template {
output: Option<String>,
#[arg(long)]
complete: bool,
},
}
#[derive(Debug, Clone, clap::Subcommand)]
pub enum UserAction {
List {
limit: Option<u32>,
#[arg(long)]
active: bool,
},
Create {
email: String,
password: Option<String>,
#[arg(long)]
admin: bool,
},
Delete {
user: String,
#[arg(long)]
force: bool,
},
SetRole {
email: String,
role: String,
},
Update {
user: String,
#[arg(long)]
email: Option<String>,
#[arg(long)]
active: Option<bool>,
},
}
#[derive(Debug, Clone, clap::Subcommand)]
pub enum ServerAction {
Status,
Start {
port: Option<u16>,
#[arg(long)]
daemon: bool,
},
Stop {
#[arg(long)]
force: bool,
},
Restart {
port: Option<u16>,
},
}
#[derive(Debug, Clone, clap::Subcommand)]
pub enum SecurityAction {
AuditLog,
ThreatReport,
ForceLogout {
user_id: String,
},
Audit {
#[arg(long, default_value = "7")]
days: u32,
#[arg(long)]
detailed: bool,
},
Sessions {
#[arg(long)]
user: Option<String>,
#[arg(long)]
terminate: Option<String>,
},
ThreatIntel {
#[arg(long)]
update: bool,
#[arg(long)]
check_ip: Option<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_status_default_fields() {
let status = ServerStatus {
web_server_state: ServerRunState::Stopped,
web_server_port: None,
last_config_update: None,
active_sessions: 0,
health_status: HealthStatus::Healthy,
started_at: chrono::Utc::now(),
};
assert_eq!(status.web_server_state, ServerRunState::Stopped);
assert_eq!(status.active_sessions, 0);
assert!(status.web_server_port.is_none());
}
#[test]
fn test_health_status_variants() {
let h = HealthStatus::Healthy;
assert!(matches!(h, HealthStatus::Healthy));
let w = HealthStatus::Warning("low memory".to_string());
assert!(matches!(w, HealthStatus::Warning(_)));
let c = HealthStatus::Critical("storage down".to_string());
assert!(matches!(c, HealthStatus::Critical(_)));
}
#[test]
fn test_server_info_creation() {
let info = ServerInfo {
version: "0.5.0".to_string(),
uptime: "1h 30m".to_string(),
status: "running".to_string(),
port: Some(8080),
active_sessions: 5,
};
assert_eq!(info.version, "0.5.0");
assert_eq!(info.active_sessions, 5);
assert_eq!(info.port, Some(8080));
}
#[test]
fn test_user_statistics_creation() {
let stats = UserStatistics {
total_users: 100,
active_sessions: 20,
failed_logins_today: 3,
new_registrations_today: 5,
};
assert_eq!(stats.total_users, 100);
assert_eq!(stats.failed_logins_today, 3);
}
#[test]
fn test_security_event_creation() {
let event = SecurityEvent {
timestamp: chrono::Utc::now(),
event_type: "LoginFailure".to_string(),
description: "Failed login attempt".to_string(),
ip_address: Some("192.168.1.1".to_string()),
user_id: Some("user123".to_string()),
};
assert_eq!(event.event_type, "LoginFailure");
assert!(event.ip_address.is_some());
assert!(event.user_id.is_some());
}
#[test]
fn test_format_uptime_since() {
let started_at = chrono::DateTime::parse_from_rfc3339("2026-03-21T10:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let now = chrono::DateTime::parse_from_rfc3339("2026-03-21T12:45:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
assert_eq!(format_uptime_since(started_at, now), "2h 45m");
}
}