use chrono::TimeDelta; use sqlx::sqlite::SqlitePool; use super::repo::auth::Provider as _; use crate::{ clock::DateTime, password::StoredHash, repo::{ error::NotFound as _, login::{Login, Provider as _}, token::Provider as _, }, }; pub struct Logins<'a> { db: &'a SqlitePool, } impl<'a> Logins<'a> { pub const fn new(db: &'a SqlitePool) -> Self { Self { db } } pub async fn login( &self, name: &str, password: &str, login_at: &DateTime, ) -> Result { let mut tx = self.db.begin().await?; let login = if let Some((login, stored_hash)) = tx.auth().for_name(name).await? { if stored_hash.verify(password)? { // Password verified; use the login. login } else { // Password NOT verified. return Err(LoginError::Rejected); } } else { let password_hash = StoredHash::new(password)?; tx.logins().create(name, &password_hash).await? }; let token = tx.tokens().issue(&login, login_at).await?; tx.commit().await?; Ok(token) } pub async fn validate(&self, secret: &str, used_at: &DateTime) -> Result { // Somewhat arbitrarily, expire after 7 days. let expire_at = used_at.to_owned() - TimeDelta::days(7); let mut tx = self.db.begin().await?; tx.tokens().expire(&expire_at).await?; let login = tx .tokens() .validate(secret, used_at) .await .not_found(|| ValidateError::InvalidToken)?; tx.commit().await?; Ok(login) } pub async fn logout(&self, secret: &str) -> Result<(), ValidateError> { let mut tx = self.db.begin().await?; tx.tokens() .revoke(secret) .await .not_found(|| ValidateError::InvalidToken)?; tx.commit().await?; Ok(()) } } #[derive(Debug, thiserror::Error)] pub enum LoginError { #[error("invalid login")] Rejected, #[error(transparent)] DatabaseError(#[from] sqlx::Error), #[error(transparent)] PasswordHashError(#[from] password_hash::Error), } #[derive(Debug, thiserror::Error)] pub enum ValidateError { #[error("invalid token")] InvalidToken, #[error(transparent)] DatabaseError(#[from] sqlx::Error), }