use chrono::TimeDelta; use sqlx::sqlite::SqlitePool; use super::{repo::Provider as _, Id, Invite, Summary}; use crate::{ clock::DateTime, db::{Duplicate as _, NotFound as _}, event::{repo::Provider as _, Broadcaster, Event}, login::{repo::Provider as _, Login, Password}, name::Name, token::{repo::Provider as _, Secret}, }; 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: &Login, 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<(Login, Secret), AcceptError> { 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 password_hash = password.hash()?; 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 created = tx.sequence().next(accepted_at).await?; let login = tx .logins() .create(name, &password_hash, &created) .await .duplicate(|| AcceptError::DuplicateLogin(name.clone()))?; let secret = tx.tokens().issue(&login, accepted_at).await?; tx.commit().await?; self.events .broadcast(login.events().map(Event::from).collect::>()); 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("name in use: {0}")] DuplicateLogin(Name), #[error(transparent)] Database(#[from] sqlx::Error), #[error(transparent)] PasswordHash(#[from] password_hash::Error), }