- 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>
216 lines
6.0 KiB
Rust
216 lines
6.0 KiB
Rust
//! 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(())
|
|
}
|