use chrono::TimeDelta; use sqlx::sqlite::SqlitePool; use super::{Id, Invite, Summary, repo::Provider as _}; use crate::{ clock::DateTime, db::{Duplicate as _, NotFound as _}, event::Broadcaster, name::Name, token::{Secret, repo::Provider as _}, user::{ Password, User, create::{self, Create}, }, }; pub struct Invites<'a> { db: &'a SqlitePool, events: &'a Broadcaster, } impl<'a> Invites<'a> { pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { Self { db, events } } pub async fn issue(&self, issuer: &User, issued_at: &DateTime) -> Result { let mut tx = self.db.begin().await?; let invite = tx.invites().create(issuer, issued_at).await?; tx.commit().await?; Ok(invite) } pub async fn get(&self, invite: &Id) -> Result, sqlx::Error> { let mut tx = self.db.begin().await?; let invite = tx.invites().summary(invite).await.optional()?; tx.commit().await?; Ok(invite) } pub async fn accept( &self, invite: &Id, name: &Name, password: &Password, accepted_at: &DateTime, ) -> Result<(User, Secret), AcceptError> { let create = Create::begin(name, password, accepted_at); let mut tx = self.db.begin().await?; let invite = tx .invites() .by_id(invite) .await .not_found(|| AcceptError::NotFound(invite.clone()))?; // Split the tx here so we don't block writes while we deal with the password, // and don't deal with the password until we're fairly confident we can accept // the invite. Final validation is in the next tx. tx.commit().await?; let validated = create.validate()?; let mut tx = self.db.begin().await?; // If the invite has been deleted or accepted in the interim, this step will // catch it. tx.invites().accept(&invite).await?; let stored = validated .store(&mut tx) .await .duplicate(|| AcceptError::DuplicateLogin(name.clone()))?; let secret = tx.tokens().issue(stored.user(), accepted_at).await?; tx.commit().await?; let login = stored.publish(self.events); Ok((login.as_created(), secret)) } pub async fn expire(&self, relative_to: &DateTime) -> Result<(), sqlx::Error> { // Somewhat arbitrarily, expire after one day. let expire_at = relative_to.to_owned() - TimeDelta::days(1); let mut tx = self.db.begin().await?; tx.invites().expire(&expire_at).await?; tx.commit().await?; Ok(()) } } #[derive(Debug, thiserror::Error)] pub enum AcceptError { #[error("invite not found: {0}")] NotFound(Id), #[error("invalid user name: {0}")] InvalidName(Name), #[error("name in use: {0}")] DuplicateLogin(Name), #[error(transparent)] Database(#[from] sqlx::Error), #[error(transparent)] PasswordHash(#[from] password_hash::Error), } impl From for AcceptError { fn from(error: create::Error) -> Self { match error { create::Error::InvalidName(name) => Self::InvalidName(name), create::Error::PasswordHash(error) => Self::PasswordHash(error), } } }