From 812f1cafe3f8a68bf45b677fade7417c30d92eac Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Fri, 11 Oct 2024 22:40:03 -0400 Subject: Create APIs for inviting users. --- src/invite/app.rs | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/invite/app.rs (limited to 'src/invite/app.rs') diff --git a/src/invite/app.rs b/src/invite/app.rs new file mode 100644 index 0000000..998b4f1 --- /dev/null +++ b/src/invite/app.rs @@ -0,0 +1,106 @@ +use chrono::TimeDelta; +use sqlx::sqlite::SqlitePool; + +use super::{repo::Provider as _, Id, Invite, Summary}; +use crate::{ + clock::DateTime, + db::NotFound as _, + event::repo::Provider as _, + login::{repo::Provider as _, Login, Password}, + token::{repo::Provider as _, Secret}, +}; + +pub struct Invites<'a> { + db: &'a SqlitePool, +} + +impl<'a> Invites<'a> { + pub const fn new(db: &'a SqlitePool) -> Self { + Self { db } + } + + pub async fn create( + &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 { + let mut tx = self.db.begin().await?; + let invite = tx + .invites() + .summary(invite) + .await + .not_found(|| Error::NotFound(invite.clone()))?; + tx.commit().await?; + + Ok(invite) + } + + pub async fn accept( + &self, + invite: &Id, + name: &str, + password: &Password, + accepted_at: &DateTime, + ) -> Result { + 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?; + let secret = tx.tokens().issue(&login, accepted_at).await?; + tx.commit().await?; + + 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 Error { + #[error("invite not found: {0}")] + NotFound(Id), + #[error(transparent)] + Database(#[from] sqlx::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum AcceptError { + #[error("invite not found: {0}")] + NotFound(Id), + #[error(transparent)] + Database(#[from] sqlx::Error), + #[error(transparent)] + PasswordHash(#[from] password_hash::Error), +} -- cgit v1.2.3