diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-10-02 01:02:58 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-10-02 01:02:58 -0400 |
| commit | 5d3392799f88c5a3d3f9c656c73d6e8ac5c4d793 (patch) | |
| tree | 426c568d82b67a98095d25952d2b5b2345a6545b /src/token/repo | |
| parent | 357116366c1307bedaac6a3dfe9c5ed8e0e0c210 (diff) | |
Split login and token handling.
Diffstat (limited to 'src/token/repo')
| -rw-r--r-- | src/token/repo/auth.rs | 50 | ||||
| -rw-r--r-- | src/token/repo/mod.rs | 4 | ||||
| -rw-r--r-- | src/token/repo/token.rs | 151 |
3 files changed, 205 insertions, 0 deletions
diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs new file mode 100644 index 0000000..b299697 --- /dev/null +++ b/src/token/repo/auth.rs @@ -0,0 +1,50 @@ +use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; + +use crate::login::{self, password::StoredHash, Login}; + +pub trait Provider { + fn auth(&mut self) -> Auth; +} + +impl<'c> Provider for Transaction<'c, Sqlite> { + fn auth(&mut self) -> Auth { + Auth(self) + } +} + +pub struct Auth<'t>(&'t mut SqliteConnection); + +impl<'t> Auth<'t> { + // Retrieves a login by name, plus its stored password hash for + // verification. If there's no login with the requested name, this will + // return [None]. + pub async fn for_name( + &mut self, + name: &str, + ) -> Result<Option<(Login, StoredHash)>, sqlx::Error> { + let found = sqlx::query!( + r#" + select + id as "id: login::Id", + name, + password_hash as "password_hash: StoredHash" + from login + where name = $1 + "#, + name, + ) + .map(|rec| { + ( + Login { + id: rec.id, + name: rec.name, + }, + rec.password_hash, + ) + }) + .fetch_optional(&mut *self.0) + .await?; + + Ok(found) + } +} diff --git a/src/token/repo/mod.rs b/src/token/repo/mod.rs new file mode 100644 index 0000000..9169743 --- /dev/null +++ b/src/token/repo/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +mod token; + +pub use self::token::Provider; diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs new file mode 100644 index 0000000..5f64dac --- /dev/null +++ b/src/token/repo/token.rs @@ -0,0 +1,151 @@ +use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use uuid::Uuid; + +use crate::{ + clock::DateTime, + login::{self, Login}, + token::{Id, Secret}, +}; + +pub trait Provider { + fn tokens(&mut self) -> Tokens; +} + +impl<'c> Provider for Transaction<'c, Sqlite> { + fn tokens(&mut self) -> Tokens { + Tokens(self) + } +} + +pub struct Tokens<'t>(&'t mut SqliteConnection); + +impl<'c> Tokens<'c> { + // Issue a new token for an existing login. The issued_at timestamp will + // be used to control expiry, until the token is actually used. + pub async fn issue( + &mut self, + login: &Login, + issued_at: &DateTime, + ) -> Result<Secret, sqlx::Error> { + let id = Id::generate(); + let secret = Uuid::new_v4().to_string(); + + let secret = sqlx::query_scalar!( + r#" + insert + into token (id, secret, login, issued_at, last_used_at) + values ($1, $2, $3, $4, $4) + returning secret as "secret!: Secret" + "#, + id, + secret, + login.id, + issued_at, + ) + .fetch_one(&mut *self.0) + .await?; + + Ok(secret) + } + + pub async fn require(&mut self, token: &Id) -> Result<(), sqlx::Error> { + sqlx::query_scalar!( + r#" + select id as "id: Id" + from token + where id = $1 + "#, + token, + ) + .fetch_one(&mut *self.0) + .await?; + + Ok(()) + } + + // Revoke a token by its secret. + pub async fn revoke(&mut self, token: &Id) -> Result<(), sqlx::Error> { + sqlx::query_scalar!( + r#" + delete + from token + where id = $1 + returning id as "id: Id" + "#, + token, + ) + .fetch_one(&mut *self.0) + .await?; + + Ok(()) + } + + // Expire and delete all tokens that haven't been used more recently than + // `expire_at`. + pub async fn expire(&mut self, expire_at: &DateTime) -> Result<Vec<Id>, sqlx::Error> { + let tokens = sqlx::query_scalar!( + r#" + delete + from token + where last_used_at < $1 + returning id as "id: Id" + "#, + expire_at, + ) + .fetch_all(&mut *self.0) + .await?; + + Ok(tokens) + } + + // Validate a token by its secret, retrieving the associated Login record. + // Will return [None] if the token is not valid. The token's last-used + // timestamp will be set to `used_at`. + pub async fn validate( + &mut self, + secret: &Secret, + used_at: &DateTime, + ) -> Result<(Id, Login), sqlx::Error> { + // I would use `update … returning` to do this in one query, but + // sqlite3, as of this writing, does not allow an update's `returning` + // clause to reference columns from tables joined into the update. Two + // queries is fine, but it feels untidy. + sqlx::query!( + r#" + update token + set last_used_at = $1 + where secret = $2 + "#, + used_at, + secret, + ) + .execute(&mut *self.0) + .await?; + + let login = sqlx::query!( + r#" + select + token.id as "token_id: Id", + login.id as "login_id: login::Id", + name as "login_name" + from login + join token on login.id = token.login + where token.secret = $1 + "#, + secret, + ) + .map(|row| { + ( + row.token_id, + Login { + id: row.login_id, + name: row.login_name, + }, + ) + }) + .fetch_one(&mut *self.0) + .await?; + + Ok(login) + } +} |
