use actix::Message; use model::{AccountId, Audience, Token, TokenId}; use crate::{db_async_handler, Result}; #[derive(Debug, Copy, Clone, PartialEq, serde::Serialize, thiserror::Error)] pub enum Error { #[error("Failed to save new token")] Create, #[error("Failed to find token by jti")] Jti, } /// 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(Message)] #[rtype(result = "Result")] pub struct TokenByJti { pub jti: uuid::Uuid, } db_async_handler!(TokenByJti, token_by_jti, Token, inner_token_by_jti); pub(crate) async fn token_by_jti( msg: TokenByJti, 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(msg.jti) .fetch_one(t) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Token(Error::Jti) }) } #[derive(Message)] #[rtype(result = "Result")] pub struct CreateToken { pub customer_id: uuid::Uuid, pub role: model::Role, pub subject: AccountId, pub audience: Audience, } db_async_handler!(CreateToken, create_token, Token, inner_create_token); pub(crate) async fn create_token( msg: CreateToken, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { let CreateToken { customer_id, role, subject, audience, } = msg; 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:?}"); crate::Error::Token(Error::Create) }) } #[derive(Message)] #[rtype(result = "Result")] pub struct CreateExtendedToken { pub customer_id: uuid::Uuid, pub role: model::Role, pub subject: AccountId, pub audience: Audience, pub expiration_time: chrono::NaiveDateTime, } db_async_handler!( CreateExtendedToken, create_extended_token, Token, inner_create_extended_token ); pub(crate) async fn create_extended_token( msg: CreateExtendedToken, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { let CreateExtendedToken { customer_id, role, subject, audience, expiration_time, } = msg; 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:?}"); crate::Error::Token(Error::Create) }) } #[derive(Message)] #[rtype(result = "Result>")] pub struct DeleteToken { pub token_id: TokenId, } db_async_handler!(DeleteToken, delete_token, Option, inner_delete_token); pub(crate) async fn delete_token( msg: DeleteToken, 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(msg.token_id) .fetch_optional(t) .await .map_err(|e| { tracing::error!("{e:?}"); crate::Error::Token(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_else(|| Role::Admin); let subject = match subject { Some(id) => id, _ => test_create_account(t).await.id, }; let audience = audience.unwrap_or_else(|| 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))); } }