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, crate::ModelColumns)]
21pub struct AdminUser {
22 #[key]
23 #[auto]
24 pub id: Uuid,
25 #[unique]
26 pub email: String,
27 #[columns(skip)]
29 pub password_hash: String,
30 pub display_name: String,
31 pub active: bool,
33 pub created_at: Timestamp,
34}
35
36impl PanelUser for AdminUser {
37 fn user_id(&self) -> String {
38 self.id.to_string()
39 }
40
41 fn display_name(&self) -> &str {
42 &self.display_name
43 }
44
45 fn can_access_panel(&self) -> bool {
46 self.active
47 }
48}
49
50#[derive(Debug, Default, Clone, Copy)]
52pub struct PasswordAuth;
53
54impl Authenticator for PasswordAuth {
55 type User = AdminUser;
56
57 async fn verify(
58 &self,
59 cx: &Cx,
60 login: &str,
61 password: &str,
62 ) -> topcoat::Result<Option<AdminUser>> {
63 let mut db = crate::db::db(cx);
64 let user = AdminUser::filter(AdminUser::fields().email().eq(login.to_string()))
65 .first()
66 .exec(&mut db)
67 .await
68 .map_err(infrastructure_failure)?;
69 let hash = user.as_ref().map(|user| user.password_hash.as_str());
70 if !verify_password(password, hash) {
71 return Ok(None);
72 }
73 Ok(user)
74 }
75
76 async fn find_by_id(&self, cx: &Cx, id: &str) -> topcoat::Result<Option<AdminUser>> {
77 let Ok(id) = Uuid::parse_str(id) else {
78 return Ok(None);
79 };
80 let mut db = crate::db::db(cx);
81 let user = AdminUser::filter(AdminUser::fields().id().eq(id))
82 .first()
83 .exec(&mut db)
84 .await
85 .map_err(infrastructure_failure)?;
86 Ok(user.filter(|user| user.active))
88 }
89}
90
91impl Registrar for PasswordAuth {
95 type Input = SignUp;
96 type User = AdminUser;
97
98 async fn validate(
99 &self,
100 _cx: &Cx,
101 sign_up: &SignUp,
102 ex: &mut dyn toasty::Executor,
103 ) -> topcoat::Result<FieldErrors> {
104 let mut errors = sign_up.password_errors();
105 let taken = AdminUser::filter(AdminUser::fields().email().eq(sign_up.email.clone()))
106 .first()
107 .exec(ex)
108 .await
109 .map_err(infrastructure_failure)?;
110 if taken.is_some() {
111 errors.add("email", "Email has already been taken");
112 }
113 Ok(errors)
114 }
115
116 async fn register(
117 &self,
118 _cx: &Cx,
119 sign_up: SignUp,
120 ex: &mut dyn toasty::Executor,
121 ) -> topcoat::Result<AdminUser> {
122 create_admin(ex, &sign_up.email, &sign_up.password, &sign_up.name).await
123 }
124}
125
126pub async fn create_admin(
141 ex: &mut dyn toasty::Executor,
142 email: &str,
143 password: &str,
144 display_name: &str,
145) -> topcoat::Result<AdminUser> {
146 let admin = toasty::create!(AdminUser {
147 email: email.to_string(),
148 password_hash: hash_password(password)?,
149 display_name: display_name.to_string(),
150 active: true,
151 created_at: Timestamp::now(),
152 })
153 .exec(ex)
154 .await?;
155 Ok(admin)
156}
157
158pub fn hash_password(password: &str) -> topcoat::Result<String> {
161 use argon2::password_hash::PasswordHasher;
162
163 argon2::Argon2::default()
164 .hash_password(password.as_bytes())
165 .map(|hash| hash.to_string())
166 .map_err(topcoat::Error::from)
167}
168
169#[must_use]
192pub fn verify_password(password: &str, hash: Option<&str>) -> bool {
193 use argon2::password_hash::{PasswordVerifier, phc::PasswordHash};
194
195 let Ok(parsed) = PasswordHash::new(hash.unwrap_or(DUMMY_PASSWORD_HASH)) else {
196 return false;
197 };
198 let verified = argon2::Argon2::default()
199 .verify_password(password.as_bytes(), &parsed)
200 .is_ok();
201 verified && hash.is_some()
202}