arc-web-cli 0.8.2

Create self-contained Arc applications
use arc_core::read_model_store::ReadModelStore;
use argon2::{
    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Argon2,
};
use rand::rngs::OsRng;
use serde::Serialize;

#[derive(Clone, Serialize)]
pub struct AuthenticatedUser {
    pub id: String,
    pub name: String,
    pub email: String,
    pub version: i64,
}

pub async fn authenticate(
    store: &dyn ReadModelStore,
    email: &str,
    password: &str,
) -> Option<AuthenticatedUser> {
    let email = email.trim().to_ascii_lowercase();
    let rows = store
        .list(crate::domain::user::projector::USERS_VIEW)
        .await
        .ok()?;
    let row = rows.into_iter().find(|row| {
        row["active"].as_bool() == Some(true)
            && row["email"]
                .as_str()
                .is_some_and(|value| value.eq_ignore_ascii_case(&email))
    })?;
    let hash = PasswordHash::new(row["password_hash"].as_str()?).ok()?;
    Argon2::default()
        .verify_password(password.as_bytes(), &hash)
        .ok()?;
    public_user(&row)
}

pub fn hash_password(password: &str) -> anyhow::Result<String> {
    if password.len() < 12 {
        anyhow::bail!("password must contain at least 12 characters");
    }
    Argon2::default()
        .hash_password(password.as_bytes(), &SaltString::generate(&mut OsRng))
        .map(|hash| hash.to_string())
        .map_err(|error| anyhow::anyhow!("password hashing failed: {error}"))
}

pub fn public_user(row: &serde_json::Value) -> Option<AuthenticatedUser> {
    Some(AuthenticatedUser {
        id: row["id"].as_str()?.to_owned(),
        name: row["name"].as_str()?.to_owned(),
        email: row["email"].as_str()?.to_owned(),
        version: row["version"].as_i64()?,
    })
}