diff options
Diffstat (limited to 'src/login/app.rs')
| -rw-r--r-- | src/login/app.rs | 114 |
1 files changed, 114 insertions, 0 deletions
diff --git a/src/login/app.rs b/src/login/app.rs new file mode 100644 index 0000000..77d4ac3 --- /dev/null +++ b/src/login/app.rs @@ -0,0 +1,114 @@ +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<Secret, LoginError> { + 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<Secret, LoginError> { + 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<login::repo::LoadError> 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(), + } + } +} |
