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

@ -40,8 +40,7 @@ fn render(map: &BTreeMap<String, u16>) -> String {
// BTreeMap → serde_json::to_string_pretty preserves key order,
// so the output is deterministic across calls with the same
// agent set.
serde_json::to_string_pretty(map)
.expect("BTreeMap<String, u16> is always serialisable")
serde_json::to_string_pretty(map).expect("BTreeMap<String, u16> is always serialisable")
}
/// Atomically write the JSON for `names` to
@ -61,12 +60,10 @@ pub fn write(names: &[String]) -> Result<()> {
return Ok(());
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create {}", parent.display()))?;
std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
}
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, &body)
.with_context(|| format!("write {}", tmp.display()))?;
std::fs::write(&tmp, &body).with_context(|| format!("write {}", tmp.display()))?;
std::fs::rename(&tmp, &path).with_context(|| {
format!(
"rename {} -> {} (atomic publish)",
@ -132,9 +129,6 @@ mod tests {
// BTreeMap sorts → alpha before zeta in output.
let alpha_pos = body.find("alpha").expect("alpha in output");
let zeta_pos = body.find("zeta").expect("zeta in output");
assert!(
alpha_pos < zeta_pos,
"sorted order broken:\n{body}"
);
assert!(alpha_pos < zeta_pos, "sorted order broken:\n{body}");
}
}