SEC-02: Implement credential rotation security improvements - Add JWT secret rotation to rotate-secret command - Generate 64-character HS512-compatible secrets - Automatic .env backup with timestamp - Atomic file updates via temp+rename pattern - Add health verification for rotated credentials - Route rotate-secret, rotate-secrets, vault commands in CLI - Add verification attempts for database and JWT endpoints Security improvements: - JWT_SECRET now rotatable (previously impossible) - Automatic rollback via backup files - Health checks catch configuration errors - Clear warnings about token invalidation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
270 lines
7.5 KiB
Rust
270 lines
7.5 KiB
Rust
use super::admin_types::*;
|
|
use crate::core::shared::state::AppState;
|
|
use crate::core::urls::ApiUrls;
|
|
use axum::{
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Json},
|
|
routing::{get, post},
|
|
};
|
|
use diesel::prelude::*;
|
|
use diesel::sql_types::{Text, Nullable};
|
|
use log::{error, info};
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
/// Get admin dashboard data
|
|
pub async fn get_admin_dashboard(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(bot_id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
let bot_id = bot_id.into_inner();
|
|
|
|
// Get system status
|
|
let (database_ok, redis_ok) = match get_system_status(&state).await {
|
|
Ok(status) => (true, status.is_healthy()),
|
|
Err(e) => {
|
|
error!("Failed to get system status: {}", e);
|
|
(false, false)
|
|
}
|
|
};
|
|
|
|
// Get user count
|
|
let user_count = get_stats_users(&state).await.unwrap_or(0);
|
|
let group_count = get_stats_groups(&state).await.unwrap_or(0);
|
|
let bot_count = get_stats_bots(&state).await.unwrap_or(0);
|
|
|
|
// Get storage stats
|
|
let storage_stats = get_stats_storage(&state).await.unwrap_or_else(|| StorageStat {
|
|
total_gb: 0,
|
|
used_gb: 0,
|
|
percent: 0.0,
|
|
});
|
|
|
|
// Get recent activities
|
|
let activities = get_dashboard_activity(&state, Some(20))
|
|
.await
|
|
.unwrap_or_default();
|
|
|
|
// Get member/bot/invitation stats
|
|
let member_count = get_dashboard_members(&state, bot_id, 50)
|
|
.await
|
|
.unwrap_or(0);
|
|
let bot_list = get_dashboard_bots(&state, bot_id, 50)
|
|
.await
|
|
.unwrap_or_default();
|
|
let invitation_count = get_dashboard_invitations(&state, bot_id, 50)
|
|
.await
|
|
.unwrap_or(0);
|
|
|
|
let dashboard_data = AdminDashboardData {
|
|
users: vec![
|
|
UserStat {
|
|
id: Uuid::new_v4(),
|
|
name: "Users".to_string(),
|
|
count: user_count as i64,
|
|
},
|
|
GroupStat {
|
|
id: Uuid::new_v4(),
|
|
name: "Groups".to_string(),
|
|
count: group_count as i64,
|
|
},
|
|
BotStat {
|
|
id: Uuid::new_v4(),
|
|
name: "Bots".to_string(),
|
|
count: bot_count as i64,
|
|
},
|
|
],
|
|
groups,
|
|
bots: bot_list,
|
|
storage: storage_stats,
|
|
activities,
|
|
invitations: vec![
|
|
UserStat {
|
|
id: Uuid::new_v4(),
|
|
name: "Members".to_string(),
|
|
count: member_count as i64,
|
|
},
|
|
UserStat {
|
|
id: Uuid::new_v4(),
|
|
name: "Invitations".to_string(),
|
|
count: invitation_count as i64,
|
|
},
|
|
],
|
|
};
|
|
|
|
(StatusCode::OK, Json(dashboard_data)).into_response()
|
|
}
|
|
|
|
/// Get system health status
|
|
pub async fn get_system_status(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> impl IntoResponse {
|
|
let (database_ok, redis_ok) = match get_system_status(&state).await {
|
|
Ok(status) => (true, status.is_healthy()),
|
|
Err(e) => {
|
|
error!("Failed to get system status: {}", e);
|
|
(false, false)
|
|
}
|
|
};
|
|
|
|
let response = SystemHealth {
|
|
database: database_ok,
|
|
redis: redis_ok,
|
|
services: vec![],
|
|
};
|
|
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
|
|
/// Get system metrics
|
|
pub async fn get_system_metrics(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> impl IntoResponse {
|
|
// Get CPU usage
|
|
let cpu_usage = sys_info::get_system_cpu_usage();
|
|
let cpu_usage_percent = if cpu_usage > 0.0 {
|
|
(cpu_usage / sys_info::get_system_cpu_count() as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Get memory usage
|
|
let mem_total = sys_info::get_total_memory_mb();
|
|
let mem_used = sys_info::get_used_memory_mb();
|
|
let mem_percent = if mem_total > 0 {
|
|
((mem_total - mem_used) as f64 / mem_total as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Get disk usage
|
|
let disk_total = sys_info::get_total_disk_space_gb();
|
|
let disk_used = sys_info::get_used_disk_space_gb();
|
|
let disk_percent = if disk_total > 0.0 {
|
|
((disk_total - disk_used) as f64 / disk_total as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let services = vec![
|
|
ServiceStatus {
|
|
name: "database".to_string(),
|
|
status: if database_ok { "running" } else { "stopped" }.to_string(),
|
|
uptime_seconds: 0,
|
|
},
|
|
ServiceStatus {
|
|
name: "redis".to_string(),
|
|
status: if redis_ok { "running" } else { "stopped" }.to_string(),
|
|
uptime_seconds: 0,
|
|
},
|
|
];
|
|
|
|
let metrics = SystemMetricsResponse {
|
|
cpu_usage,
|
|
memory_total_mb: mem_total,
|
|
memory_used_mb: mem_used,
|
|
memory_percent: mem_percent,
|
|
disk_total_gb: disk_total,
|
|
disk_used_gb: disk_used,
|
|
disk_percent: disk_percent,
|
|
network_in_mbps: 0.0,
|
|
network_out_mbps: 0.0,
|
|
active_connections: 0,
|
|
request_rate_per_minute: 0,
|
|
error_rate_percent: 0.0,
|
|
};
|
|
|
|
(StatusCode::OK, Json(metrics)).into_response()
|
|
}
|
|
|
|
/// Get user statistics
|
|
pub async fn get_stats_users(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> impl IntoResponse {
|
|
use crate::core::shared::models::schema::users;
|
|
|
|
let count = users::table
|
|
.count()
|
|
.get_result(&state.conn)
|
|
.map_err(|e| format!("Failed to get user count: {}", e))?;
|
|
|
|
let response = vec![
|
|
UserStat {
|
|
id: Uuid::new_v4(),
|
|
name: "Total Users".to_string(),
|
|
count: count as i64,
|
|
},
|
|
];
|
|
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
|
|
/// Get group statistics
|
|
pub async fn get_stats_groups(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> impl IntoResponse {
|
|
use crate::core::shared::models::schema::bot_groups;
|
|
|
|
let count = bot_groups::table
|
|
.count()
|
|
.get_result(&state.conn)
|
|
.map_err(|e| format!("Failed to get group count: {}", e))?;
|
|
|
|
let response = vec![
|
|
UserStat {
|
|
id: Uuid::new_v4(),
|
|
name: "Total Groups".to_string(),
|
|
count: count as i64,
|
|
},
|
|
];
|
|
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
|
|
/// Get bot statistics
|
|
pub async fn get_stats_bots(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> impl IntoResponse {
|
|
use crate::core::shared::models::schema::bots;
|
|
|
|
let count = bots::table
|
|
.count()
|
|
.get_result(&state.conn)
|
|
.map_err(|e| format!("Failed to get bot count: {}", e))?;
|
|
|
|
let response = vec![
|
|
UserStat {
|
|
id: Uuid::new_v4(),
|
|
name: "Total Bots".to_string(),
|
|
count: count as i64,
|
|
},
|
|
];
|
|
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
|
|
/// Get storage statistics
|
|
pub async fn get_stats_storage(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> impl IntoResponse {
|
|
use crate::core::shared::models::schema::storage_usage;
|
|
|
|
let usage = storage_usage::table
|
|
.limit(100)
|
|
.order_by(crate::core::shared::models::schema::storage_usage::timestamp.desc())
|
|
.load(&state.conn)
|
|
.map_err(|e| format!("Failed to get storage stats: {}", e))?;
|
|
|
|
let total_gb = usage.iter().map(|u| u.total_gb.unwrap_or(0.0)).sum::<f64>();
|
|
let used_gb = usage.iter().map(|u| u.used_gb.unwrap_or(0.0)).sum::<f64>();
|
|
let percent = if total_gb > 0.0 { (used_gb / total_gb * 100.0) } else { 0.0 };
|
|
|
|
let response = StorageStat {
|
|
total_gb: total_gb.round(),
|
|
used_gb: used_gb.round(),
|
|
percent: (percent * 100.0).round(),
|
|
};
|
|
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|