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,215 @@
|
||||
//! Administrator repository operations.
|
||||
|
||||
use crate::error::{DbError, Result};
|
||||
use crate::models::{format_datetime, parse_datetime};
|
||||
use chrono::Utc;
|
||||
use nx9_wg_core::types::auth::Admin;
|
||||
use sqlx::{Row, SqlitePool};
|
||||
|
||||
/// Retrieve the single administrator record, if initialized.
|
||||
pub async fn get_admin(pool: &SqlitePool) -> Result<Option<Admin>> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT id, username, password_hash, totp_secret, totp_enabled,
|
||||
last_login_at, last_login_ip, created_at, updated_at
|
||||
FROM admin
|
||||
WHERE id = 1
|
||||
"#,
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(DbError::Sqlx)?;
|
||||
|
||||
match row {
|
||||
Some(r) => {
|
||||
let id: i64 = r.try_get("id")?;
|
||||
let username: String = r.try_get("username")?;
|
||||
let password_hash: String = r.try_get("password_hash")?;
|
||||
let totp_secret: Option<String> = r.try_get("totp_secret")?;
|
||||
let totp_enabled_int: i64 = r.try_get("totp_enabled")?;
|
||||
let last_login_at_str: Option<String> = r.try_get("last_login_at")?;
|
||||
let last_login_ip: Option<String> = r.try_get("last_login_ip")?;
|
||||
let created_at_str: String = r.try_get("created_at")?;
|
||||
let updated_at_str: String = r.try_get("updated_at")?;
|
||||
|
||||
let last_login_at = match last_login_at_str {
|
||||
Some(s) => Some(parse_datetime(&s)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Some(Admin {
|
||||
id,
|
||||
username,
|
||||
password_hash,
|
||||
totp_secret,
|
||||
totp_enabled: totp_enabled_int != 0,
|
||||
last_login_at,
|
||||
last_login_ip,
|
||||
created_at: parse_datetime(&created_at_str)?,
|
||||
updated_at: parse_datetime(&updated_at_str)?,
|
||||
}))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the administrator record by username.
|
||||
pub async fn get_admin_by_username(pool: &SqlitePool, username: &str) -> Result<Option<Admin>> {
|
||||
let admin = get_admin(pool).await?;
|
||||
match admin {
|
||||
Some(a) if a.username == username => Ok(Some(a)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the single administrator has already been initialized.
|
||||
pub async fn admin_exists(pool: &SqlitePool) -> Result<bool> {
|
||||
let row = sqlx::query("SELECT COUNT(*) as count FROM admin WHERE id = 1")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(DbError::Sqlx)?;
|
||||
|
||||
let count: i64 = row.try_get("count")?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Create the single administrator record.
|
||||
///
|
||||
/// Fails if an administrator already exists.
|
||||
pub async fn create_admin(pool: &SqlitePool, username: &str, password_hash: &str) -> Result<Admin> {
|
||||
if admin_exists(pool).await? {
|
||||
return Err(DbError::Conflict(
|
||||
"Administrator has already been initialized".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
let now_str = format_datetime(&now);
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO admin (id, username, password_hash, totp_secret, totp_enabled, created_at, updated_at)
|
||||
VALUES (1, ?, ?, NULL, 0, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(username)
|
||||
.bind(password_hash)
|
||||
.bind(&now_str)
|
||||
.bind(&now_str)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| match &e {
|
||||
sqlx::Error::Database(dbe) if dbe.is_unique_violation() => {
|
||||
DbError::Conflict("Administrator already exists or username conflict".to_string())
|
||||
}
|
||||
_ => DbError::Sqlx(e),
|
||||
})?;
|
||||
|
||||
Ok(Admin {
|
||||
id: 1,
|
||||
username: username.to_string(),
|
||||
password_hash: password_hash.to_string(),
|
||||
totp_secret: None,
|
||||
totp_enabled: false,
|
||||
last_login_at: None,
|
||||
last_login_ip: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the administrator's password hash.
|
||||
pub async fn update_admin_password(pool: &SqlitePool, new_password_hash: &str) -> Result<()> {
|
||||
let now = Utc::now().naive_utc();
|
||||
let now_str = format_datetime(&now);
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE admin
|
||||
SET password_hash = ?, updated_at = ?
|
||||
WHERE id = 1
|
||||
"#,
|
||||
)
|
||||
.bind(new_password_hash)
|
||||
.bind(&now_str)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(DbError::Sqlx)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DbError::NotFound(
|
||||
"Administrator record does not exist".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update administrator TOTP configuration.
|
||||
pub async fn update_admin_totp(
|
||||
pool: &SqlitePool,
|
||||
totp_secret: Option<&str>,
|
||||
totp_enabled: bool,
|
||||
) -> Result<()> {
|
||||
let now = Utc::now().naive_utc();
|
||||
let now_str = format_datetime(&now);
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE admin
|
||||
SET totp_secret = ?, totp_enabled = ?, updated_at = ?
|
||||
WHERE id = 1
|
||||
"#,
|
||||
)
|
||||
.bind(totp_secret)
|
||||
.bind(if totp_enabled { 1 } else { 0 })
|
||||
.bind(&now_str)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(DbError::Sqlx)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DbError::NotFound(
|
||||
"Administrator record does not exist".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record a successful administrator login timestamp and IP address.
|
||||
pub async fn record_admin_login(pool: &SqlitePool, ip_address: Option<&str>) -> Result<()> {
|
||||
let now = Utc::now().naive_utc();
|
||||
let now_str = format_datetime(&now);
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE admin
|
||||
SET last_login_at = ?, last_login_ip = ?, updated_at = ?
|
||||
WHERE id = 1
|
||||
"#,
|
||||
)
|
||||
.bind(&now_str)
|
||||
.bind(ip_address)
|
||||
.bind(&now_str)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(DbError::Sqlx)?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(DbError::NotFound(
|
||||
"Administrator record does not exist".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete administrator record (if explicitly supported).
|
||||
pub async fn delete_admin(pool: &SqlitePool) -> Result<()> {
|
||||
sqlx::query("DELETE FROM admin WHERE id = 1")
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(DbError::Sqlx)?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user