use sqlx::sqlite::SqlitePool; use crate::{ clock::DateTime, db::NotFound as _, login::{self, Login, repo::Provider as _}, name::{self, Name}, password::Password, token::{Broadcaster, Event as TokenEvent, Secret, Token, repo::Provider as _}, }; pub struct Logins<'a> { db: &'a SqlitePool, token_events: &'a Broadcaster, } impl<'a> Logins<'a> { pub const fn new(db: &'a SqlitePool, token_events: &'a Broadcaster) -> Self { Self { db, token_events } } pub async fn with_password( &self, name: &Name, candidate: &Password, login_at: &DateTime, ) -> Result { let mut tx = self.db.begin().await?; let (login, password) = tx .logins() .by_name(name) .await .not_found(|| LoginError::Rejected)?; // Split the transaction here to avoid holding the tx open (potentially blocking // other writes) while we do the fairly expensive task of verifying the // password. It's okay if the token issuance transaction happens some notional // amount of time after retrieving the login, as inserting the token will fail // if the account is deleted during that time. tx.commit().await?; if password.verify(candidate)? { let mut tx = self.db.begin().await?; let (token, secret) = Token::generate(&login, login_at); tx.tokens().create(&token, &secret).await?; tx.commit().await?; Ok(secret) } else { Err(LoginError::Rejected) } } pub async fn change_password( &self, login: &Login, from: &Password, to: &Password, changed_at: &DateTime, ) -> Result { let mut tx = self.db.begin().await?; let (login, password) = tx .logins() .by_id(&login.id) .await .not_found(|| LoginError::Rejected)?; // Split the transaction here to avoid holding the tx open (potentially blocking // other writes) while we do the fairly expensive task of verifying the // password. It's okay if the token issuance transaction happens some notional // amount of time after retrieving the login, as inserting the token will fail // if the account is deleted during that time. tx.commit().await?; if password.verify(from)? { let to_hash = to.hash()?; let (token, secret) = Token::generate(&login, changed_at); let mut tx = self.db.begin().await?; tx.logins().set_password(&login, &to_hash).await?; let revoked = tx.tokens().revoke_all(&login).await?; tx.tokens().create(&token, &secret).await?; tx.commit().await?; for event in revoked.into_iter().map(TokenEvent::Revoked) { self.token_events.broadcast(event); } Ok(secret) } else { Err(LoginError::Rejected) } } } #[derive(Debug, thiserror::Error)] pub enum LoginError { #[error("invalid login")] Rejected, #[error(transparent)] Database(#[from] sqlx::Error), #[error(transparent)] Name(#[from] name::Error), #[error(transparent)] PasswordHash(#[from] password_hash::Error), } impl From for LoginError { fn from(error: login::repo::LoadError) -> Self { use login::repo::LoadError; match error { LoadError::Database(error) => error.into(), LoadError::Name(error) => error.into(), } } }