cli: avoid data-dir initialization for version; create db parent dirs; redact generated passwords in CLI output
- Prevent 'nx9-wg version' from creating data directories by avoiding database initialization. - Create parent directories when an explicit --database path is provided. - Redact printed generated administrator passwords; announce file path or redact instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
commit
2ac6c81dfe
140 files changed
+31342
No files matched your search
@@ -0,0 +1,235 @@
|
||||
//! Peer management table and responsive card component.
|
||||
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use nx9_wg_core::types::wireguard::{Peer, PeerProfile, PeerState, PeerType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Formatted peer row for table and mobile card representation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PeerRowView {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub peer_type: PeerType,
|
||||
pub profile: PeerProfile,
|
||||
pub state: PeerState,
|
||||
pub is_online: bool,
|
||||
pub status_label: String,
|
||||
pub status_class: String,
|
||||
pub ipv4: String,
|
||||
pub ipv6: String,
|
||||
pub public_key_full: String,
|
||||
pub public_key_truncated: String,
|
||||
pub rx_bytes_formatted: String,
|
||||
pub tx_bytes_formatted: String,
|
||||
pub last_handshake_relative: String,
|
||||
pub expires_at: Option<NaiveDateTime>,
|
||||
pub is_expired: bool,
|
||||
}
|
||||
|
||||
impl PeerRowView {
|
||||
pub fn from_peer_and_telemetry(
|
||||
peer: &Peer,
|
||||
rx_bytes: u64,
|
||||
tx_bytes: u64,
|
||||
last_handshake: Option<NaiveDateTime>,
|
||||
) -> Self {
|
||||
let now = Utc::now().naive_utc();
|
||||
let is_expired =
|
||||
peer.state == PeerState::Expired || peer.expires_at.is_some_and(|exp| exp <= now);
|
||||
|
||||
let is_online = if is_expired || peer.state != PeerState::Active {
|
||||
false
|
||||
} else if let Some(hs) = last_handshake.or(peer.last_handshake_at) {
|
||||
let diff = now.signed_duration_since(hs);
|
||||
diff.num_seconds() >= 0 && diff.num_seconds() < 180
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let (status_label, status_class) = if is_expired {
|
||||
("Expired".to_string(), "status-fail".to_string())
|
||||
} else {
|
||||
match peer.state {
|
||||
PeerState::Active => {
|
||||
if is_online {
|
||||
("Online".to_string(), "status-pass".to_string())
|
||||
} else {
|
||||
("Offline".to_string(), "status-neutral".to_string())
|
||||
}
|
||||
}
|
||||
PeerState::Disabled => ("Disabled".to_string(), "status-warning".to_string()),
|
||||
PeerState::Revoked => ("Revoked".to_string(), "status-fail".to_string()),
|
||||
PeerState::Expired => ("Expired".to_string(), "status-fail".to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
let pub_key_str = peer.public_key.as_str();
|
||||
let pub_key_truncated = if pub_key_str.len() > 12 {
|
||||
format!(
|
||||
"{}...{}",
|
||||
&pub_key_str[..6],
|
||||
&pub_key_str[pub_key_str.len() - 6..]
|
||||
)
|
||||
} else {
|
||||
pub_key_str.to_string()
|
||||
};
|
||||
|
||||
let last_handshake_relative = match last_handshake.or(peer.last_handshake_at) {
|
||||
Some(hs) => format_relative_time(hs, now),
|
||||
None => "Never".to_string(),
|
||||
};
|
||||
|
||||
Self {
|
||||
id: peer.id,
|
||||
name: peer.name.clone(),
|
||||
peer_type: peer.peer_type,
|
||||
profile: peer.profile,
|
||||
state: peer.state,
|
||||
is_online,
|
||||
status_label,
|
||||
status_class,
|
||||
ipv4: peer
|
||||
.address_v4
|
||||
.map(|ip| ip.to_string())
|
||||
.unwrap_or_else(|| "—".to_string()),
|
||||
ipv6: peer
|
||||
.address_v6
|
||||
.map(|ip| ip.to_string())
|
||||
.unwrap_or_else(|| "—".to_string()),
|
||||
public_key_full: pub_key_str.to_string(),
|
||||
public_key_truncated: pub_key_truncated,
|
||||
rx_bytes_formatted: format_bytes(rx_bytes),
|
||||
tx_bytes_formatted: format_bytes(tx_bytes),
|
||||
last_handshake_relative,
|
||||
expires_at: peer.expires_at,
|
||||
is_expired,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter state for peer table.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct PeerTableFilter {
|
||||
pub search_query: String,
|
||||
pub status_filter: Option<String>,
|
||||
}
|
||||
|
||||
impl PeerTableFilter {
|
||||
pub fn matches(&self, row: &PeerRowView) -> bool {
|
||||
if let Some(s) = self.status_filter.as_ref().filter(|s| !s.is_empty())
|
||||
&& !row.status_label.eq_ignore_ascii_case(s)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if !self.search_query.trim().is_empty() {
|
||||
let q = self.search_query.to_lowercase();
|
||||
let name_match = row.name.to_lowercase().contains(&q);
|
||||
let ip_match = row.ipv4.contains(&q) || row.ipv6.contains(&q);
|
||||
let key_match = row.public_key_full.to_lowercase().contains(&q);
|
||||
if !name_match && !ip_match && !key_match {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Format bytes into human readable binary units (B, KB, MB, GB, TB).
|
||||
pub fn format_bytes(bytes: u64) -> String {
|
||||
const KB: u64 = 1024;
|
||||
const MB: u64 = KB * 1024;
|
||||
const GB: u64 = MB * 1024;
|
||||
const TB: u64 = GB * 1024;
|
||||
|
||||
if bytes >= TB {
|
||||
format!("{:.2} TB", bytes as f64 / TB as f64)
|
||||
} else if bytes >= GB {
|
||||
format!("{:.2} GB", bytes as f64 / GB as f64)
|
||||
} else if bytes >= MB {
|
||||
format!("{:.2} MB", bytes as f64 / MB as f64)
|
||||
} else if bytes >= KB {
|
||||
format!("{:.1} KB", bytes as f64 / KB as f64)
|
||||
} else {
|
||||
format!("{bytes} B")
|
||||
}
|
||||
}
|
||||
|
||||
/// Format relative time between a past timestamp and reference now.
|
||||
pub fn format_relative_time(past: NaiveDateTime, now: NaiveDateTime) -> String {
|
||||
let diff = now.signed_duration_since(past);
|
||||
let seconds = diff.num_seconds();
|
||||
|
||||
if seconds < 0 {
|
||||
"Just now".to_string()
|
||||
} else if seconds < 60 {
|
||||
format!("{seconds}s ago")
|
||||
} else if seconds < 3600 {
|
||||
format!("{}m ago", seconds / 60)
|
||||
} else if seconds < 86400 {
|
||||
format!("{}h ago", seconds / 3600)
|
||||
} else {
|
||||
format!("{}d ago", seconds / 86400)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nx9_wg_core::types::wireguard::WireGuardPublicKey;
|
||||
|
||||
#[test]
|
||||
fn test_format_bytes() {
|
||||
assert_eq!(format_bytes(500), "500 B");
|
||||
assert_eq!(format_bytes(1024), "1.0 KB");
|
||||
assert_eq!(format_bytes(1048576), "1.00 MB");
|
||||
assert_eq!(format_bytes(1073741824), "1.00 GB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_peer_row_view_and_filtering() {
|
||||
let peer = Peer {
|
||||
id: Uuid::new_v4(),
|
||||
interface_id: Uuid::new_v4(),
|
||||
name: "alice-laptop".to_string(),
|
||||
peer_type: PeerType::RoadWarrior,
|
||||
state: PeerState::Active,
|
||||
public_key: WireGuardPublicKey::new(
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string(),
|
||||
),
|
||||
private_key: None,
|
||||
preshared_key: None,
|
||||
endpoint: None,
|
||||
allowed_ips: "10.0.0.2/32".to_string(),
|
||||
server_allowed_ips: None,
|
||||
address_v4: Some("10.0.0.2/32".parse().unwrap()),
|
||||
address_v6: None,
|
||||
dns: None,
|
||||
mtu: Some(1420),
|
||||
persistent_keepalive: Some(25),
|
||||
profile: PeerProfile::FullTunnel,
|
||||
expires_at: None,
|
||||
last_handshake_at: None,
|
||||
created_at: Utc::now().naive_utc(),
|
||||
updated_at: Utc::now().naive_utc(),
|
||||
};
|
||||
|
||||
let row = PeerRowView::from_peer_and_telemetry(&peer, 1024, 2048, None);
|
||||
assert_eq!(row.name, "alice-laptop");
|
||||
assert_eq!(row.status_label, "Offline");
|
||||
assert_eq!(row.rx_bytes_formatted, "1.0 KB");
|
||||
assert_eq!(row.tx_bytes_formatted, "2.0 KB");
|
||||
|
||||
let filter = PeerTableFilter {
|
||||
search_query: "alice".to_string(),
|
||||
status_filter: None,
|
||||
};
|
||||
assert!(filter.matches(&row));
|
||||
|
||||
let non_matching = PeerTableFilter {
|
||||
search_query: "bob".to_string(),
|
||||
status_filter: None,
|
||||
};
|
||||
assert!(!non_matching.matches(&row));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user