use jiff::Timestamp;
use topcoat::context::Cx;
use uuid::Uuid;
use super::{Authenticator, PanelUser, Registrar, SignUp, infrastructure_failure};
use crate::FieldErrors;
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))
}
}
impl Registrar for PasswordAuth {
type Input = SignUp;
type User = AdminUser;
async fn validate(
&self,
_cx: &Cx,
sign_up: &SignUp,
ex: &mut dyn toasty::Executor,
) -> topcoat::Result<FieldErrors> {
let mut errors = sign_up.password_errors();
let taken = AdminUser::filter(AdminUser::fields().email().eq(sign_up.email.clone()))
.first()
.exec(ex)
.await
.map_err(infrastructure_failure)?;
if taken.is_some() {
errors.add("email", "Email has already been taken");
}
Ok(errors)
}
async fn register(
&self,
_cx: &Cx,
sign_up: SignUp,
ex: &mut dyn toasty::Executor,
) -> topcoat::Result<AdminUser> {
create_admin(ex, &sign_up.email, &sign_up.password, &sign_up.name).await
}
}
pub async fn create_admin(
ex: &mut dyn toasty::Executor,
email: &str,
password: &str,
display_name: &str,
) -> topcoat::Result<AdminUser> {
let admin = toasty::create!(AdminUser {
email: email.to_string(),
password_hash: hash_password(password)?,
display_name: display_name.to_string(),
active: true,
created_at: Timestamp::now(),
})
.exec(ex)
.await?;
Ok(admin)
}
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()
}