use jiff::Timestamp;
use topcoat::context::Cx;
use uuid::Uuid;
use super::{Authenticator, PanelUser, infrastructure_failure};
const DUMMY_PASSWORD_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$h3oXdPBVwhcgZ1OTO/PuzQ$zLrHLgIkwhqu4ZlLTfSyB8mPuL6mAtaswv/eXJ5ADO8";
#[derive(Debug, Clone, toasty::Model)]
pub struct AdminUser {
#[key]
#[auto]
pub id: Uuid,
#[unique]
pub email: String,
pub password_hash: String,
pub display_name: String,
pub active: bool,
pub created_at: Timestamp,
}
impl PanelUser for AdminUser {
fn user_id(&self) -> String {
self.id.to_string()
}
fn display_name(&self) -> &str {
&self.display_name
}
fn can_access_panel(&self) -> bool {
self.active
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct PasswordAuth;
impl Authenticator for PasswordAuth {
type User = AdminUser;
async fn verify(
&self,
cx: &Cx,
login: &str,
password: &str,
) -> topcoat::Result<Option<AdminUser>> {
let mut db = crate::db::db(cx);
let user = AdminUser::filter(AdminUser::fields().email().eq(login.to_string()))
.first()
.exec(&mut db)
.await
.map_err(infrastructure_failure)?;
let hash = user.as_ref().map(|user| user.password_hash.as_str());
if !verify_password(password, hash) {
return Ok(None);
}
Ok(user)
}
async fn find_by_id(&self, cx: &Cx, id: &str) -> topcoat::Result<Option<AdminUser>> {
let Ok(id) = Uuid::parse_str(id) else {
return Ok(None);
};
let mut db = crate::db::db(cx);
let user = AdminUser::filter(AdminUser::fields().id().eq(id))
.first()
.exec(&mut db)
.await
.map_err(infrastructure_failure)?;
Ok(user.filter(|user| user.active))
}
}
pub fn hash_password(password: &str) -> topcoat::Result<String> {
use argon2::password_hash::PasswordHasher;
argon2::Argon2::default()
.hash_password(password.as_bytes())
.map(|hash| hash.to_string())
.map_err(topcoat::Error::from)
}
#[must_use]
pub fn verify_password(password: &str, hash: Option<&str>) -> bool {
use argon2::password_hash::{PasswordVerifier, phc::PasswordHash};
let Ok(parsed) = PasswordHash::new(hash.unwrap_or(DUMMY_PASSWORD_HASH)) else {
return false;
};
let verified = argon2::Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.is_ok();
verified && hash.is_some()
}