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

@ -59,9 +59,7 @@ async fn read_lock_at_tag(repo: &Path, tag: &str) -> Result<Option<String>> {
// `inputs = { }`. Any other git failure (permission denied,
// ref-not-found, etc.) propagates as a hard error rather than
// being silently swallowed.
if stderr.contains("does not exist")
|| stderr.contains("exists on disk, but not in")
{
if stderr.contains("does not exist") || stderr.contains("exists on disk, but not in") {
return Ok(None);
}
anyhow::bail!("git show {spec} failed: {}", stderr.trim());
@ -130,10 +128,7 @@ pub fn duplicate_groups(raw: &str) -> Result<Vec<DuplicateGroup>> {
});
entry.keys.push(name.clone());
}
let mut dups: Vec<DuplicateGroup> = groups
.into_values()
.filter(|g| g.keys.len() > 1)
.collect();
let mut dups: Vec<DuplicateGroup> = groups.into_values().filter(|g| g.keys.len() > 1).collect();
for g in &mut dups {
g.keys.sort();
}
@ -259,12 +254,7 @@ async fn lock_in_sync_inner(worktree: &Path) -> Result<()> {
async fn remove_worktree(repo: &Path, worktree: &Path) -> Result<()> {
let out = git_command()
.current_dir(repo)
.args([
"worktree",
"remove",
"--force",
&worktree.to_string_lossy(),
])
.args(["worktree", "remove", "--force", &worktree.to_string_lossy()])
.output()
.await
.with_context(|| format!("git worktree remove {}", worktree.display()))?;