use chrono::TimeDelta; use sqlx::sqlite::SqlitePool; use super::{Id, Invite, Summary, repo::Provider as _}; use crate::{ clock::DateTime, db::{self, NotFound as _}, error::failed::{ErrorExt as _, Failed, ResultExt as _}, event::{Broadcaster, repo::Provider as _}, login::Login, name::Name, password::Password, token::{Secret, Token, repo::Provider as _}, user::{self, create, create::Create, repo::Provider as _}, }; pub struct Invites { db: SqlitePool, events: Broadcaster, } impl Invites { pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } pub async fn issue(&self, issuer: &Login, issued_at: &DateTime) -> Result { let issuer_not_found = || IssueError::IssuerNotFound(issuer.id.clone().into()); let issuer_deleted = || IssueError::IssuerDeleted(issuer.id.clone().into()); let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?; let issuer = tx .users() .by_login(issuer) .await .optional() .fail("Failed to load issuing user")? .ok_or_else(issuer_not_found)?; let now = tx .sequence() .current() .await .fail("Failed to find event sequence number")?; let issuer = issuer.as_of(now).ok_or_else(issuer_deleted)?; let invite = tx .invites() .create(&issuer, issued_at) .await .fail("Failed to store new invitation")?; tx.commit().await.fail(db::failed::COMMIT)?; 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 { let to_not_found = || AcceptError::NotFound(invite.clone()); let create = Create::begin(name, password, accepted_at); let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?; let invite = tx .invites() .by_id(invite) .await .optional() .fail("Failed to load invitation")? .ok_or_else(to_not_found)?; // 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.fail(db::failed::COMMIT)?; let validated = create.validate().map_err(|err| match err { create::Error::InvalidName(name) => AcceptError::InvalidName(name), create::Error::Failed(_) => err.fail("Failed to validate invited user"), })?; let mut tx = self.db.begin().await.fail(db::failed::BEGIN)?; // If the invite has been deleted or accepted in the interim, this step will // catch it. tx.invites() .accept(&invite) .await .fail("Failed to remove accepted invitation")?; let stored = validated .store(&mut tx) .await .map_err(|err| match err.as_database_error() { Some(err) if err.is_unique_violation() => { AcceptError::DuplicateLogin(name.clone()) } _ => err.fail("Failed to store invited user"), })?; let login = stored.login(); let (token, secret) = Token::generate(login, accepted_at); tx.tokens() .create(&token, &secret) .await .fail("Failed to issue token for invited user")?; tx.commit().await.fail(db::failed::COMMIT)?; stored.publish(&self.events); Ok(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 IssueError { #[error("issuing user {0} not found")] IssuerNotFound(user::Id), #[error("issuing user {0} deleted")] IssuerDeleted(user::Id), #[error(transparent)] Failed(#[from] Failed), } #[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)] Failed(#[from] Failed), }