1use std::sync::{Arc, LazyLock};
5
6use jiff::Timestamp;
7use tokio::sync::Semaphore;
8use topcoat::context::Cx;
9use uuid::Uuid;
10
11use super::{
12 Authenticator, EmailChange, EmailVerifier, PanelUser, PasswordResetter, Profile, ProfileEditor,
13 ProfileUpdate, Registrar, SignUp, infrastructure_failure, profile::CURRENT_PASSWORD_FIELD,
14 request_email_change,
15};
16use crate::FieldErrors;
17
18const DUMMY_PASSWORD_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$h3oXdPBVwhcgZ1OTO/PuzQ$zLrHLgIkwhqu4ZlLTfSyB8mPuL6mAtaswv/eXJ5ADO8";
22
23#[derive(Debug, Clone, toasty::Model, crate::ModelColumns)]
34pub struct AdminUser {
35 #[key]
36 #[auto]
37 pub id: Uuid,
38 #[unique]
39 pub email: String,
40 #[columns(skip)]
42 pub password_hash: String,
43 pub display_name: String,
44 pub active: bool,
46 pub email_verified_at: Option<Timestamp>,
50 pub created_at: Timestamp,
51}
52
53impl PanelUser for AdminUser {
54 fn user_id(&self) -> String {
55 self.id.to_string()
56 }
57
58 fn display_name(&self) -> &str {
59 &self.display_name
60 }
61
62 fn email(&self) -> Option<&str> {
63 Some(&self.email)
64 }
65
66 fn can_access_panel(&self) -> bool {
67 self.active
68 }
69}
70
71pub(crate) async fn end_changed_credentials(
77 cx: &Cx,
78 ex: &mut dyn toasty::Executor,
79 before: &(dyn std::any::Any + Send + Sync),
80 after: &(dyn std::any::Any + Send + Sync),
81) -> topcoat::Result<()> {
82 let (Some(before), Some(after)) = (
83 before.downcast_ref::<AdminUser>(),
84 after.downcast_ref::<AdminUser>(),
85 ) else {
86 return Ok(());
87 };
88 if before.email == after.email && before.password_hash == after.password_hash {
89 return Ok(());
90 }
91 let Some(panel) = super::realm::current_realm(cx) else {
92 return Ok(());
93 };
94 let user_type = std::any::TypeId::of::<AdminUser>();
95 let panels = super::session::credential_panels(cx, user_type, panel);
96 if panels.is_empty() {
97 return Ok(());
98 }
99 let user_id = before.id.to_string();
100 if let Some(signed) = super::signed(cx).filter(|signed| {
101 signed.user.user_id() == user_id && panels.iter().any(|p| Arc::ptr_eq(p, &signed.realm))
102 }) {
103 let session = topcoat::session::start(cx).await?;
104 return super::session::replace_sessions(cx, ex, signed, user_type, &session).await;
105 }
106 super::session::end_credentials(ex, &user_id, &panels, super::token::REVOCATION).await
107}
108
109#[derive(Debug, Default, Clone, Copy)]
120pub struct PasswordAuth;
121
122impl Authenticator for PasswordAuth {
123 type User = AdminUser;
124
125 async fn verify(
126 &self,
127 cx: &Cx,
128 login: &str,
129 password: &str,
130 ) -> topcoat::Result<Option<AdminUser>> {
131 let mut db = crate::db::db(cx)?;
132 let user = AdminUser::filter(AdminUser::fields().email().eq(normalize_email(login)))
133 .first()
134 .exec(&mut db)
135 .await
136 .map_err(infrastructure_failure)?;
137 let hash = user.as_ref().map(|user| user.password_hash.as_str());
138 if !verify_password(password, hash).await {
139 return Ok(None);
140 }
141 Ok(user)
142 }
143
144 async fn find_by_id(&self, cx: &Cx, id: &str) -> topcoat::Result<Option<AdminUser>> {
145 let Ok(id) = Uuid::parse_str(id) else {
146 return Ok(None);
147 };
148 let mut db = crate::db::db(cx)?;
149 let user = AdminUser::filter(AdminUser::fields().id().eq(id))
150 .first()
151 .exec(&mut db)
152 .await
153 .map_err(infrastructure_failure)?;
154 Ok(user.filter(|user| user.active))
156 }
157}
158
159#[derive(Debug, Clone, Copy)]
175pub struct PasswordSignUp {
176 active: bool,
177}
178
179impl PasswordSignUp {
180 #[must_use]
182 pub fn pending() -> Self {
183 Self { active: false }
184 }
185
186 #[must_use]
189 pub fn open() -> Self {
190 Self { active: true }
191 }
192}
193
194impl Registrar for PasswordSignUp {
195 type Input = SignUp;
196 type User = AdminUser;
197
198 async fn validate(
199 &self,
200 _cx: &Cx,
201 sign_up: &SignUp,
202 ex: &mut dyn toasty::Executor,
203 ) -> topcoat::Result<FieldErrors> {
204 let mut errors = sign_up.password_errors();
205 let taken = AdminUser::filter(
206 AdminUser::fields()
207 .email()
208 .eq(normalize_email(&sign_up.email)),
209 )
210 .first()
211 .exec(ex)
212 .await
213 .map_err(infrastructure_failure)?;
214 if taken.is_some() {
215 errors.add("email", email_taken());
216 }
217 Ok(errors)
218 }
219
220 async fn register(
221 &self,
222 _cx: &Cx,
223 sign_up: SignUp,
224 ex: &mut dyn toasty::Executor,
225 ) -> topcoat::Result<AdminUser> {
226 insert_admin(
227 ex,
228 &sign_up.email,
229 &sign_up.password,
230 &sign_up.name,
231 self.active,
232 None,
233 )
234 .await
235 }
236}
237
238impl ProfileEditor for PasswordAuth {
247 type Input = Profile;
248 type User = AdminUser;
249
250 fn fill(&self, _cx: &Cx, admin: &AdminUser) -> Profile {
251 Profile {
252 name: admin.display_name.clone(),
253 email: admin.email.clone(),
254 password: None,
255 password_confirmation: String::new(),
256 current_password: None,
257 }
258 }
259
260 async fn validate(
261 &self,
262 _cx: &Cx,
263 admin: &AdminUser,
264 profile: &Profile,
265 ex: &mut dyn toasty::Executor,
266 ) -> topcoat::Result<FieldErrors> {
267 let mut errors = profile.password_errors();
268 let new_email = normalize_email(&profile.email) != admin.email;
269 if new_email || profile.password.is_some() {
270 match &profile.current_password {
271 None => errors.add(
272 CURRENT_PASSWORD_FIELD,
273 crate::text!("tablo-profile-current-password-needed"),
274 ),
275 Some(current) if !verify_password(current, Some(&admin.password_hash)).await => {
276 errors.add(
277 CURRENT_PASSWORD_FIELD,
278 crate::text!("tablo-profile-current-password-wrong"),
279 );
280 }
281 Some(_) => {}
282 }
283 }
284 if new_email && !errors.contains_key(CURRENT_PASSWORD_FIELD) {
287 let taken = AdminUser::filter(
288 AdminUser::fields()
289 .email()
290 .eq(normalize_email(&profile.email)),
291 )
292 .first()
293 .exec(ex)
294 .await
295 .map_err(infrastructure_failure)?;
296 if taken.is_some_and(|other| other.id != admin.id) {
297 errors.add("email", email_taken());
298 }
299 }
300 Ok(errors)
301 }
302
303 async fn save(
304 &self,
305 cx: &Cx,
306 admin: &AdminUser,
307 profile: Profile,
308 ex: &mut dyn toasty::Executor,
309 ) -> topcoat::Result<ProfileUpdate> {
310 let email = normalize_email(&profile.email);
311 let stored = email != admin.email
312 && request_email_change(cx, &mut *ex, admin, &email).await? == EmailChange::Immediate;
313 let credentials = stored || profile.password.is_some();
314 let mut update = AdminUser::filter(AdminUser::fields().id().eq(admin.id))
315 .update()
316 .display_name(profile.name);
317 if stored {
318 update.set_email(email);
319 update.set_email_verified_at(None);
320 }
321 if let Some(password) = &profile.password {
322 update.set_password_hash(hash_password(password).await?);
323 }
324 update.exec(ex).await?;
325 Ok(if credentials {
326 ProfileUpdate::Credentials
327 } else {
328 ProfileUpdate::Details
329 })
330 }
331}
332
333impl PasswordResetter for PasswordAuth {
336 type User = AdminUser;
337
338 async fn find_by_email(&self, cx: &Cx, email: &str) -> topcoat::Result<Option<AdminUser>> {
339 let mut db = crate::db::db(cx)?;
340 AdminUser::filter(AdminUser::fields().email().eq(normalize_email(email)))
341 .first()
342 .exec(&mut db)
343 .await
344 .map_err(infrastructure_failure)
345 }
346
347 async fn reset_password(
348 &self,
349 _cx: &Cx,
350 user_id: &str,
351 password: &str,
352 ex: &mut dyn toasty::Executor,
353 ) -> topcoat::Result<()> {
354 let id = Uuid::parse_str(user_id).map_err(|_| topcoat::router::error::not_found())?;
355 AdminUser::filter(AdminUser::fields().id().eq(id))
356 .update()
357 .password_hash(hash_password(password).await?)
358 .exec(ex)
359 .await?;
360 Ok(())
361 }
362}
363
364impl EmailVerifier for PasswordAuth {
367 type User = AdminUser;
368
369 fn is_verified(&self, admin: &AdminUser) -> bool {
370 admin.email_verified_at.is_some()
371 }
372
373 async fn store_verified_email(
374 &self,
375 _cx: &Cx,
376 user_id: &str,
377 email: &str,
378 ex: &mut dyn toasty::Executor,
379 ) -> topcoat::Result<()> {
380 let id = Uuid::parse_str(user_id).map_err(|_| topcoat::router::error::not_found())?;
381 AdminUser::filter(AdminUser::fields().id().eq(id))
382 .update()
383 .email(normalize_email(email))
384 .email_verified_at(Some(Timestamp::now()))
385 .exec(ex)
386 .await?;
387 Ok(())
388 }
389}
390
391pub async fn create_admin(
407 ex: &mut dyn toasty::Executor,
408 email: &str,
409 password: &str,
410 display_name: &str,
411) -> topcoat::Result<AdminUser> {
412 insert_admin(
413 ex,
414 email,
415 password,
416 display_name,
417 true,
418 Some(Timestamp::now()),
419 )
420 .await
421}
422
423async fn insert_admin(
425 ex: &mut dyn toasty::Executor,
426 email: &str,
427 password: &str,
428 display_name: &str,
429 active: bool,
430 verified_at: Option<Timestamp>,
431) -> topcoat::Result<AdminUser> {
432 let admin = toasty::create!(AdminUser {
433 email: normalize_email(email),
434 password_hash: hash_password(password).await?,
435 display_name: display_name.to_string(),
436 active,
437 email_verified_at: verified_at,
438 created_at: Timestamp::now(),
439 })
440 .exec(ex)
441 .await?;
442 Ok(admin)
443}
444
445fn normalize_email(email: &str) -> String {
447 email.trim().to_lowercase()
448}
449
450pub async fn hash_password(password: &str) -> topcoat::Result<String> {
458 use argon2::password_hash::PasswordHasher;
459
460 let password = password.to_string();
461 off_executor(move || {
462 argon2::Argon2::default()
463 .hash_password(password.as_bytes())
464 .map(|hash| hash.to_string())
465 .map_err(topcoat::Error::from)
466 })
467 .await
468}
469
470#[must_use]
497pub async fn verify_password(password: &str, hash: Option<&str>) -> bool {
498 use argon2::password_hash::{PasswordVerifier, phc::PasswordHash};
499
500 let known = hash.is_some();
501 let password = password.to_string();
502 let hash = hash.unwrap_or(DUMMY_PASSWORD_HASH).to_string();
503 let verified = off_executor(move || match PasswordHash::new(&hash) {
504 Ok(parsed) => argon2::Argon2::default()
505 .verify_password(password.as_bytes(), &parsed)
506 .is_ok(),
507 Err(error) => {
510 tracing::error!(error = %error, "a stored password hash does not parse");
511 false
512 }
513 })
514 .await;
515 verified && known
516}
517
518static ARGON2_PERMITS: LazyLock<Arc<Semaphore>> = LazyLock::new(|| {
522 let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
523 Arc::new(Semaphore::new(cores))
524});
525
526async fn off_executor<T: Send + 'static>(work: impl FnOnce() -> T + Send + 'static) -> T {
529 permitted(&ARGON2_PERMITS, work).await
530}
531
532pub(super) async fn permitted<T: Send + 'static>(
535 permits: &Arc<Semaphore>,
536 work: impl FnOnce() -> T + Send + 'static,
537) -> T {
538 let permit = Arc::clone(permits)
539 .acquire_owned()
540 .await
541 .expect("the Argon2 semaphore is never closed");
542 let work = move || {
543 let value = work();
544 drop(permit);
545 value
546 };
547 match tokio::task::spawn_blocking(work).await {
548 Ok(value) => value,
549 Err(error) => std::panic::resume_unwind(error.into_panic()),
550 }
551}
552
553fn email_taken() -> crate::Text {
555 crate::text!(
556 "tablo-field-taken",
557 label = crate::text!("tablo-auth-email")
558 )
559}