- 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>
112 lines
3.2 KiB
Rust
112 lines
3.2 KiB
Rust
//! Single Administrator account, password, API tokens, and session management view model.
|
|
|
|
use chrono::NaiveDateTime;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Active session view row.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SessionRowView {
|
|
pub id: String,
|
|
pub ip_address: String,
|
|
pub user_agent: String,
|
|
pub created_at: NaiveDateTime,
|
|
pub expires_at: NaiveDateTime,
|
|
pub is_current: bool,
|
|
}
|
|
|
|
/// Active API token view row.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ApiTokenRowView {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub created_at: NaiveDateTime,
|
|
pub expires_at: Option<NaiveDateTime>,
|
|
pub last_used_at: Option<NaiveDateTime>,
|
|
pub is_revoked: bool,
|
|
}
|
|
|
|
/// Administrator management view state.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AdministratorState {
|
|
// Card 1: Account
|
|
pub username: String,
|
|
pub last_login_at: Option<NaiveDateTime>,
|
|
pub last_login_ip: Option<String>,
|
|
pub totp_enabled: bool,
|
|
pub active_session_count: usize,
|
|
|
|
// Card 2: Password change
|
|
pub current_password: String,
|
|
pub new_password: String,
|
|
pub confirm_password: String,
|
|
|
|
// Card 3: API Tokens
|
|
pub new_token_name: String,
|
|
pub new_token_expires_days: Option<u32>,
|
|
pub created_token_plaintext: Option<String>,
|
|
pub tokens: Vec<ApiTokenRowView>,
|
|
|
|
// Card 4: Sessions
|
|
pub sessions: Vec<SessionRowView>,
|
|
|
|
// Feedback
|
|
pub success_message: Option<String>,
|
|
pub error_message: Option<String>,
|
|
}
|
|
|
|
impl Default for AdministratorState {
|
|
fn default() -> Self {
|
|
Self {
|
|
username: "admin".to_string(),
|
|
last_login_at: None,
|
|
last_login_ip: None,
|
|
totp_enabled: false,
|
|
active_session_count: 1,
|
|
current_password: String::new(),
|
|
new_password: String::new(),
|
|
confirm_password: String::new(),
|
|
new_token_name: String::new(),
|
|
new_token_expires_days: Some(30),
|
|
created_token_plaintext: None,
|
|
tokens: Vec::new(),
|
|
sessions: Vec::new(),
|
|
success_message: None,
|
|
error_message: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AdministratorState {
|
|
pub fn validate_password_change(&self) -> Result<(), &'static str> {
|
|
if self.current_password.is_empty() {
|
|
return Err("Current password is required");
|
|
}
|
|
if self.new_password.len() < 8 {
|
|
return Err("New password must be at least 8 characters long");
|
|
}
|
|
if self.new_password != self.confirm_password {
|
|
return Err("New password and confirmation do not match");
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_admin_password_validation() {
|
|
let mut state = AdministratorState::default();
|
|
assert!(state.validate_password_change().is_err());
|
|
|
|
state.current_password = "OldPassword123!".to_string();
|
|
state.new_password = "NewPassword123!".to_string();
|
|
state.confirm_password = "MismatchPassword!".to_string();
|
|
assert!(state.validate_password_change().is_err());
|
|
|
|
state.confirm_password = "NewPassword123!".to_string();
|
|
assert!(state.validate_password_change().is_ok());
|
|
}
|
|
}
|