use crate::AuthAction;
use anyhow::{Context, Result};
use krab_core::credentials::hash_password;
use std::io::Read as _;
pub(super) fn dispatch_auth_action(action: &AuthAction) -> Result<()> {
match action {
AuthAction::HashPassword { password, username } => {
run_hash_password(password.as_deref(), username.as_deref())
}
}
}
fn run_hash_password(password: Option<&str>, username: Option<&str>) -> Result<()> {
let password = match password {
Some(value) => value.to_string(),
None => read_password_from_stdin()?,
};
anyhow::ensure!(
!password.trim().is_empty(),
"refusing to hash an empty password"
);
let hash = hash_password(&password)?;
match username {
Some(username) => println!("{}", serde_json::json!({ username: hash })),
None => println!("{hash}"),
}
Ok(())
}
fn read_password_from_stdin() -> Result<String> {
let mut buffer = String::new();
std::io::stdin()
.read_to_string(&mut buffer)
.context("failed to read password from stdin")?;
Ok(trim_one_line_ending(&buffer).to_string())
}
fn trim_one_line_ending(value: &str) -> &str {
match value.strip_suffix('\n') {
Some(rest) => rest.strip_suffix('\r').unwrap_or(rest),
None => value,
}
}
#[cfg(test)]
mod tests {
use super::trim_one_line_ending;
use krab_core::credentials::{hash_password, is_valid_password_hash, verify_password};
#[test]
fn only_one_line_ending_is_trimmed_and_spaces_survive() {
assert_eq!(trim_one_line_ending("pw\n"), "pw");
assert_eq!(trim_one_line_ending("pw\r\n"), "pw");
assert_eq!(trim_one_line_ending("pw"), "pw");
assert_eq!(trim_one_line_ending("pw \n"), "pw ");
assert_eq!(trim_one_line_ending("pw\n\n"), "pw\n");
}
#[test]
fn hashed_output_is_a_verifiable_phc_string() {
let hash = hash_password("operator-supplied").unwrap();
assert!(is_valid_password_hash(&hash));
assert!(verify_password("operator-supplied", &hash).unwrap());
assert!(!verify_password("something-else", &hash).unwrap());
}
#[test]
fn username_form_emits_a_parseable_single_entry_map() {
let hash = hash_password("pw").unwrap();
let rendered = serde_json::json!({ "admin": hash }).to_string();
let parsed: std::collections::BTreeMap<String, String> =
serde_json::from_str(&rendered).unwrap();
assert_eq!(parsed.len(), 1);
assert!(is_valid_password_hash(&parsed["admin"]));
}
}