From e0fae4ede82eca12eeda90519e69037f57652cd5 Mon Sep 17 00:00:00 2001 From: eraden Date: Tue, 20 Dec 2022 22:20:30 +0100 Subject: [PATCH] Add database actions --- crates/token_manager/src/db/mod.rs | 3 + crates/token_manager/src/db/tokens.rs | 285 ++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 crates/token_manager/src/db/tokens.rs diff --git a/crates/token_manager/src/db/mod.rs b/crates/token_manager/src/db/mod.rs index 036573c..275ac42 100644 --- a/crates/token_manager/src/db/mod.rs +++ b/crates/token_manager/src/db/mod.rs @@ -2,6 +2,9 @@ use config::SharedAppConfig; use sqlx_core::pool::Pool; use sqlx_core::postgres::Postgres; +pub mod tokens; +pub use tokens::*; + #[derive(Clone)] pub struct Database { pub pool: sqlx::PgPool, diff --git a/crates/token_manager/src/db/tokens.rs b/crates/token_manager/src/db/tokens.rs new file mode 100644 index 0000000..7c544a6 --- /dev/null +++ b/crates/token_manager/src/db/tokens.rs @@ -0,0 +1,285 @@ +use actix::Message; +use model::{AccountId, Audience, Token, TokenId}; + +use crate::{db_async_handler, Result}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] +pub enum Error { + #[error("Failed to save new token")] + Create, + #[error("Failed to find token by jti")] + Jti, +} + +pub type Result = std::result::Result; + +/// Find token by JTI field +/// +/// # Examples +/// +/// ``` +/// use actix::Addr; +/// use database_manager::{Database, TokenByJti}; +/// +/// async fn find(db: Addr) { +/// match db.send(TokenByJti { +/// jti: uuid::Uuid::new_v4() +/// }).await { +/// Ok(Ok(token)) => { println!("{:?}", token); } +/// Ok(Err(db_err)) => { println!("{:?}", db_err); } +/// Err(actor_err) => { println!("{:?}", actor_err); } +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct TokenByJti { + pub jti: uuid::Uuid, +} + +impl TokenByJti { + pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + sqlx::query_as(r#" +SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id +FROM tokens +WHERE jwt_id = $1 AND expiration_time > now() + "#) + .bind(self.jti) + .fetch_one(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Jti + }) + } +} + +#[derive(Debug, Clone)] +pub struct CreateToken { + pub customer_id: uuid::Uuid, + pub role: model::Role, + pub subject: AccountId, + pub audience: Audience, +} + +impl CreateToken { + pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + let CreateToken { + customer_id, + role, + subject, + audience, + } = self; + sqlx::query_as(r#" +INSERT INTO tokens (customer_id, role, subject, audience) +VALUES ($1, $2, $3, $4) +RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id + "#) + .bind(customer_id) + .bind(role) + .bind(subject) + .bind(audience) + .fetch_one(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Create + }) + } +} + +#[derive(Debug, Clone)] +pub struct CreateExtendedToken { + pub customer_id: uuid::Uuid, + pub role: model::Role, + pub subject: AccountId, + pub audience: Audience, + pub expiration_time: chrono::NaiveDateTime, +} + +impl CreateExtendedToken { + pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + let CreateExtendedToken { + customer_id, + role, + subject, + audience, + expiration_time, + } = self; + sqlx::query_as(r#" +INSERT INTO tokens (customer_id, role, subject, audience, expiration_time) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id + "#) + .bind(customer_id) + .bind(role) + .bind(subject) + .bind(audience) + .bind(expiration_time) + .fetch_one(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Create + }) + } +} + +#[derive(Debug, Clone)] +pub struct DeleteToken { + pub token_id: TokenId, +} + +impl DeleteToken { + pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result> { + sqlx::query_as(r#" +DELETE FROM tokens +WHERE id = $1 +RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id + "#) + .bind(self.token_id) + .fetch_optional(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + Error::Jti + }) + } +} + +// #[cfg(test)] +// mod tests { +// use config::UpdateConfig; +// use fake::Fake; +// use model::*; +// use uuid::Uuid; +// +// use crate::*; +// +// pub struct NoOpts; +// +// impl UpdateConfig for NoOpts {} +// +// async fn test_create_account(t: &mut sqlx::Transaction<'_, +// sqlx::Postgres>) -> FullAccount { use fake::faker::internet::en; +// let login: String = en::Username().fake(); +// let email: String = en::FreeEmail().fake(); +// let hash: String = en::Password(10..20).fake(); +// +// crate::create_account( +// CreateAccount { +// email: Email::new(email), +// login: Login::new(login), +// pass_hash: PassHash::new(hash), +// role: Role::Admin, +// }, +// t, +// ) +// .await +// .unwrap() +// } +// +// async fn test_create_token_extended( +// t: &mut sqlx::Transaction<'_, sqlx::Postgres>, +// customer_id: Option, +// role: Option, +// subject: Option, +// audience: Option, +// expiration_time: Option, +// ) -> Token { +// let customer_id = customer_id.unwrap_or_else(Uuid::new_v4); +// let role = role.unwrap_or(Role::Admin); +// let subject = match subject { +// Some(id) => id, +// _ => test_create_account(t).await.id, +// }; +// let audience = audience.unwrap_or(Audience::Web); +// let expiration_time = expiration_time +// .unwrap_or_else(|| (chrono::Utc::now() + +// chrono::Duration::days(60)).naive_utc()); +// +// super::create_extended_token( +// CreateExtendedToken { +// customer_id, +// role, +// subject, +// audience, +// expiration_time, +// }, +// t, +// ) +// .await +// .unwrap() +// } +// +// #[actix::test] +// async fn create_token() { +// testx::db_t_ref!(t); +// +// super::create_token( +// CreateToken { +// customer_id: Uuid::new_v4(), +// role: Role::Admin, +// subject: test_create_account(&mut t).await.id, +// audience: Audience::Web, +// }, +// &mut t, +// ) +// .await +// .unwrap(); +// } +// +// #[actix::test] +// async fn create_extended_token() { +// testx::db_t_ref!(t); +// +// test_create_account(&mut t).await; +// +// testx::db_rollback!(t); +// } +// +// #[actix::test] +// async fn find_by_jti() { +// testx::db_t_ref!(t); +// +// let original = test_create_token_extended(&mut t, None, None, None, +// None, None).await; +// +// let found = super::token_by_jti( +// TokenByJti { +// jti: original.jwt_id, +// }, +// &mut t, +// ) +// .await +// .unwrap(); +// +// testx::db_rollback!(t); +// assert_eq!(found, original); +// } +// +// #[actix::test] +// async fn find_by_jti_expired() { +// testx::db_t_ref!(t); +// +// let original = test_create_token_extended( +// &mut t, +// None, +// None, +// None, +// None, +// Some((chrono::Utc::now() - +// chrono::Duration::seconds(1)).naive_utc()), ) +// .await; +// +// let found = super::token_by_jti( +// TokenByJti { +// jti: original.jwt_id, +// }, +// &mut t, +// ) +// .await; +// +// testx::db_rollback!(t); +// assert_eq!(found, Err(crate::Error::Token(super::Error::Jti))); +// } +// }