tablo_core/auth/
password.rs1use jiff::Timestamp;
5use topcoat::context::Cx;
6use uuid::Uuid;
7
8use super::{Authenticator, PanelUser, Registrar, SignUp, infrastructure_failure};
9use crate::FieldErrors;
10
11const DUMMY_PASSWORD_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$h3oXdPBVwhcgZ1OTO/PuzQ$zLrHLgIkwhqu4ZlLTfSyB8mPuL6mAtaswv/eXJ5ADO8";
15
16#[derive(Debug, Clone, toasty::Model)]
19pub struct AdminUser {
20 #[key]
21 #[auto]
22 pub id: Uuid,
23 #[unique]
24 pub email: String,
25 pub password_hash: String,
27 pub display_name: String,
28 pub active: bool,
30 pub created_at: Timestamp,
31}
32
33impl PanelUser for AdminUser {
34 fn user_id(&self) -> String {
35 self.id.to_string()
36 }
37
38 fn display_name(&self) -> &str {
39 &self.display_name
40 }
41
42 fn can_access_panel(&self) -> bool {
43 self.active
44 }
45}
46
47#[derive(Debug, Default, Clone, Copy)]
49pub struct PasswordAuth;
50
51impl Authenticator for PasswordAuth {
52 type User = AdminUser;
53
54 async fn verify(
55 &self,
56 cx: &Cx,
57 login: &str,
58 password: &str,
59 ) -> topcoat::Result<Option<AdminUser>> {
60 let mut db = crate::db::db(cx);
61 let user = AdminUser::filter(AdminUser::fields().email().eq(login.to_string()))
62 .first()
63 .exec(&mut db)
64 .await
65 .map_err(infrastructure_failure)?;
66 let hash = user.as_ref().map(|user| user.password_hash.as_str());
67 if !verify_password(password, hash) {
68 return Ok(None);
69 }
70 Ok(user)
71 }
72
73 async fn find_by_id(&self, cx: &Cx, id: &str) -> topcoat::Result<Option<AdminUser>> {
74 let Ok(id) = Uuid::parse_str(id) else {
75 return Ok(None);
76 };
77 let mut db = crate::db::db(cx);
78 let user = AdminUser::filter(AdminUser::fields().id().eq(id))
79 .first()
80 .exec(&mut db)
81 .await
82 .map_err(infrastructure_failure)?;
83 Ok(user.filter(|user| user.active))
85 }
86}
87
88impl Registrar for PasswordAuth {
92 type Input = SignUp;
93 type User = AdminUser;
94
95 async fn validate(
96 &self,
97 _cx: &Cx,
98 sign_up: &SignUp,
99 ex: &mut dyn toasty::Executor,
100 ) -> topcoat::Result<FieldErrors> {
101 let mut errors = sign_up.password_errors();
102 let taken = AdminUser::filter(AdminUser::fields().email().eq(sign_up.email.clone()))
103 .first()
104 .exec(ex)
105 .await
106 .map_err(infrastructure_failure)?;
107 if taken.is_some() {
108 errors.add("email", "Email has already been taken");
109 }
110 Ok(errors)
111 }
112
113 async fn register(
114 &self,
115 _cx: &Cx,
116 sign_up: SignUp,
117 ex: &mut dyn toasty::Executor,
118 ) -> topcoat::Result<AdminUser> {
119 create_admin(ex, &sign_up.email, &sign_up.password, &sign_up.name).await
120 }
121}
122
123pub async fn create_admin(
138 ex: &mut dyn toasty::Executor,
139 email: &str,
140 password: &str,
141 display_name: &str,
142) -> topcoat::Result<AdminUser> {
143 let admin = toasty::create!(AdminUser {
144 email: email.to_string(),
145 password_hash: hash_password(password)?,
146 display_name: display_name.to_string(),
147 active: true,
148 created_at: Timestamp::now(),
149 })
150 .exec(ex)
151 .await?;
152 Ok(admin)
153}
154
155pub fn hash_password(password: &str) -> topcoat::Result<String> {
158 use argon2::password_hash::PasswordHasher;
159
160 argon2::Argon2::default()
161 .hash_password(password.as_bytes())
162 .map(|hash| hash.to_string())
163 .map_err(topcoat::Error::from)
164}
165
166#[must_use]
189pub fn verify_password(password: &str, hash: Option<&str>) -> bool {
190 use argon2::password_hash::{PasswordVerifier, phc::PasswordHash};
191
192 let Ok(parsed) = PasswordHash::new(hash.unwrap_or(DUMMY_PASSWORD_HASH)) else {
193 return false;
194 };
195 let verified = argon2::Argon2::default()
196 .verify_password(password.as_bytes(), &parsed)
197 .is_ok();
198 verified && hash.is_some()
199}