feat(gateway): hivectl gateway user management + fix htpasswdFile assertion

Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.

Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.

Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
This commit is contained in:
atlas 2026-06-01 23:00:38 +02:00
commit 4bff450343
61 changed files with 1084 additions and 547 deletions

View file

@ -23,7 +23,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::coordinator::Coordinator;
const VACUUM_INTERVAL: Duration = Duration::from_hours(1);
const VACUUM_INTERVAL: Duration = Duration::from_secs(3600);
/// Keep completed task files for 48 hours before sweeping them.
const KEEP_SECS: i64 = 48 * 3600;
@ -65,7 +65,9 @@ fn sweep_once() {
/// files removed (each represents one task; `.out`/`.err` deletions
/// are not counted separately).
fn vacuum_dir(dir: &Path, cutoff: i64) -> u64 {
let Ok(rd) = std::fs::read_dir(dir) else { return 0 };
let Ok(rd) = std::fs::read_dir(dir) else {
return 0;
};
let mut removed: u64 = 0;
for entry in rd.flatten() {
let path = entry.path();
@ -97,7 +99,10 @@ fn should_delete(json_path: &Path, cutoff: i64) -> bool {
if !TERMINAL_STATUSES.contains(&status) {
return false;
}
let completed_at = v.get("completed_at").and_then(serde_json::Value::as_i64).unwrap_or(i64::MAX);
let completed_at = v
.get("completed_at")
.and_then(|t| t.as_i64())
.unwrap_or(i64::MAX);
completed_at < cutoff
}
@ -106,10 +111,10 @@ fn should_delete(json_path: &Path, cutoff: i64) -> bool {
fn delete_trio(dir: &Path, stem: &str) {
for ext in ["json", "out", "err"] {
let path = dir.join(format!("{stem}.{ext}"));
if path.exists()
&& let Err(e) = std::fs::remove_file(&path)
{
tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed");
if path.exists() {
if let Err(e) = std::fs::remove_file(&path) {
tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed");
}
}
}
}