summaryrefslogtreecommitdiff
path: root/src/invite/app.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/invite/app.rs')
-rw-r--r--src/invite/app.rs106
1 files changed, 106 insertions, 0 deletions
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<Invite, sqlx::Error> {
+ 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<Summary, Error> {
+ 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<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?;
+ 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),
+}