summaryrefslogtreecommitdiff
path: root/src/login/app.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/login/app.rs')
-rw-r--r--src/login/app.rs114
1 files changed, 114 insertions, 0 deletions
diff --git a/src/login/app.rs b/src/login/app.rs
new file mode 100644
index 0000000..77d4ac3
--- /dev/null
+++ b/src/login/app.rs
@@ -0,0 +1,114 @@
+use sqlx::sqlite::SqlitePool;
+
+use crate::{
+ clock::DateTime,
+ db::NotFound as _,
+ login::{self, Login, repo::Provider as _},
+ name::{self, Name},
+ password::Password,
+ token::{Broadcaster, Event as TokenEvent, Secret, Token, repo::Provider as _},
+};
+
+pub struct Logins<'a> {
+ db: &'a SqlitePool,
+ token_events: &'a Broadcaster,
+}
+
+impl<'a> Logins<'a> {
+ pub const fn new(db: &'a SqlitePool, token_events: &'a Broadcaster) -> Self {
+ Self { db, token_events }
+ }
+
+ pub async fn with_password(
+ &self,
+ name: &Name,
+ candidate: &Password,
+ login_at: &DateTime,
+ ) -> Result<Secret, LoginError> {
+ let mut tx = self.db.begin().await?;
+ let (login, password) = tx
+ .logins()
+ .by_name(name)
+ .await
+ .not_found(|| LoginError::Rejected)?;
+ // Split the transaction here to avoid holding the tx open (potentially blocking
+ // other writes) while we do the fairly expensive task of verifying the
+ // password. It's okay if the token issuance transaction happens some notional
+ // amount of time after retrieving the login, as inserting the token will fail
+ // if the account is deleted during that time.
+ tx.commit().await?;
+
+ if password.verify(candidate)? {
+ let mut tx = self.db.begin().await?;
+ let (token, secret) = Token::generate(&login, login_at);
+ tx.tokens().create(&token, &secret).await?;
+ tx.commit().await?;
+ Ok(secret)
+ } else {
+ Err(LoginError::Rejected)
+ }
+ }
+
+ pub async fn change_password(
+ &self,
+ login: &Login,
+ from: &Password,
+ to: &Password,
+ changed_at: &DateTime,
+ ) -> Result<Secret, LoginError> {
+ let mut tx = self.db.begin().await?;
+ let (login, password) = tx
+ .logins()
+ .by_id(&login.id)
+ .await
+ .not_found(|| LoginError::Rejected)?;
+ // Split the transaction here to avoid holding the tx open (potentially blocking
+ // other writes) while we do the fairly expensive task of verifying the
+ // password. It's okay if the token issuance transaction happens some notional
+ // amount of time after retrieving the login, as inserting the token will fail
+ // if the account is deleted during that time.
+ tx.commit().await?;
+
+ if password.verify(from)? {
+ let to_hash = to.hash()?;
+ let (token, secret) = Token::generate(&login, changed_at);
+
+ let mut tx = self.db.begin().await?;
+ tx.logins().set_password(&login, &to_hash).await?;
+
+ let revoked = tx.tokens().revoke_all(&login).await?;
+ tx.tokens().create(&token, &secret).await?;
+ tx.commit().await?;
+
+ for event in revoked.into_iter().map(TokenEvent::Revoked) {
+ self.token_events.broadcast(event);
+ }
+
+ Ok(secret)
+ } else {
+ Err(LoginError::Rejected)
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum LoginError {
+ #[error("invalid login")]
+ Rejected,
+ #[error(transparent)]
+ Database(#[from] sqlx::Error),
+ #[error(transparent)]
+ Name(#[from] name::Error),
+ #[error(transparent)]
+ PasswordHash(#[from] password_hash::Error),
+}
+
+impl From<login::repo::LoadError> for LoginError {
+ fn from(error: login::repo::LoadError) -> Self {
+ use login::repo::LoadError;
+ match error {
+ LoadError::Database(error) => error.into(),
+ LoadError::Name(error) => error.into(),
+ }
+ }
+}