tablo_core/auth/
password.rs1use jiff::Timestamp;
5use topcoat::context::Cx;
6use uuid::Uuid;
7
8use super::{Authenticator, PanelUser, infrastructure_failure};
9
10const DUMMY_PASSWORD_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$h3oXdPBVwhcgZ1OTO/PuzQ$zLrHLgIkwhqu4ZlLTfSyB8mPuL6mAtaswv/eXJ5ADO8";
14
15#[derive(Debug, Clone, toasty::Model)]
18pub struct AdminUser {
19 #[key]
20 #[auto]
21 pub id: Uuid,
22 #[unique]
23 pub email: String,
24 pub password_hash: String,
26 pub display_name: String,
27 pub active: bool,
29 pub created_at: Timestamp,
30}
31
32impl PanelUser for AdminUser {
33 fn user_id(&self) -> String {
34 self.id.to_string()
35 }
36
37 fn display_name(&self) -> &str {
38 &self.display_name
39 }
40
41 fn can_access_panel(&self) -> bool {
42 self.active
43 }
44}
45
46#[derive(Debug, Default, Clone, Copy)]
48pub struct PasswordAuth;
49
50impl Authenticator for PasswordAuth {
51 type User = AdminUser;
52
53 async fn verify(
54 &self,
55 cx: &Cx,
56 login: &str,
57 password: &str,
58 ) -> topcoat::Result<Option<AdminUser>> {
59 let mut db = crate::db::db(cx);
60 let user = AdminUser::filter(AdminUser::fields().email().eq(login.to_string()))
61 .first()
62 .exec(&mut db)
63 .await
64 .map_err(infrastructure_failure)?;
65 let hash = user.as_ref().map(|user| user.password_hash.as_str());
66 if !verify_password(password, hash) {
67 return Ok(None);
68 }
69 Ok(user)
70 }
71
72 async fn find_by_id(&self, cx: &Cx, id: &str) -> topcoat::Result<Option<AdminUser>> {
73 let Ok(id) = Uuid::parse_str(id) else {
74 return Ok(None);
75 };
76 let mut db = crate::db::db(cx);
77 let user = AdminUser::filter(AdminUser::fields().id().eq(id))
78 .first()
79 .exec(&mut db)
80 .await
81 .map_err(infrastructure_failure)?;
82 Ok(user.filter(|user| user.active))
84 }
85}
86
87pub fn hash_password(password: &str) -> topcoat::Result<String> {
90 use argon2::password_hash::PasswordHasher;
91
92 argon2::Argon2::default()
93 .hash_password(password.as_bytes())
94 .map(|hash| hash.to_string())
95 .map_err(topcoat::Error::from)
96}
97
98#[must_use]
109pub fn verify_password(password: &str, hash: Option<&str>) -> bool {
110 use argon2::password_hash::{PasswordVerifier, phc::PasswordHash};
111
112 let Ok(parsed) = PasswordHash::new(hash.unwrap_or(DUMMY_PASSWORD_HASH)) else {
113 return false;
114 };
115 let verified = argon2::Argon2::default()
116 .verify_password(password.as_bytes(), &parsed)
117 .is_ok();
118 verified && hash.is_some()
119}