use sqlx::sqlite::SqlitePool; use crate::{ clock::DateTime, db, db::NotFound as _, error::failed::{Failed, ResultExt as _}, login::{Login, repo::Provider as _}, name::Name, password::Password, push::repo::Provider as _, token::{Broadcaster, Event as TokenEvent, Secret, Token, repo::Provider as _}, }; pub struct Logins { db: SqlitePool, token_events: Broadcaster, } impl Logins { pub const fn new(db: SqlitePool, token_events: 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.fail(db::failed::BEGIN)?; let (login, password) = tx .logins() .by_name(name) .await .optional() .fail("Failed to load login")? .ok_or_else(|| 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.fail(db::failed::COMMIT)?; if password .verify(candidate) .fail("Failed to verify password")? { let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?; let (token, secret) = Token::generate(&login, login_at); tx.tokens() .create(&token, &secret) .await .fail("Failed to create token")?; tx.commit().await.fail(db::failed::COMMIT)?; 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.fail(db::failed::BEGIN)?; let (login, password) = tx .logins() .by_id(&login.id) .await .optional() .fail("Failed to load login")? .ok_or_else(|| 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.fail(db::failed::COMMIT)?; if password .verify(from) .fail("Failed to verify current password")? { let to_hash = to.hash().fail("Failed to digest new password")?; let (token, secret) = Token::generate(&login, changed_at); let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?; tx.logins() .set_password(&login, &to_hash) .await .fail("Failed to store new password")?; tx.push() .unsubscribe_login(&login) .await .fail("Failed to remove push notification subscriptions")?; let revoked = tx .tokens() .revoke_all(&login) .await .fail("Failed to revoke existing tokens")?; tx.tokens() .create(&token, &secret) .await .fail("Failed to create new token")?; tx.commit().await.fail(db::failed::COMMIT)?; revoked .into_iter() .map(TokenEvent::Revoked) .for_each(|event| self.token_events.broadcast(event)); Ok(secret) } else { Err(LoginError::Rejected) } } } #[derive(Debug, thiserror::Error)] pub enum LoginError { #[error("invalid login")] Rejected, #[error(transparent)] Failed(#[from] Failed), }