anzar-shared 0.9.21

Anzar is a lightweight authentication and authorization framework that runs as a separate microservice
Documentation
use std::sync::Arc;

use chrono::Utc;

use crate::error::{AuthError, CoreError, Result, TokenErrorType};

use super::model::RefreshToken;
use super::ports::database::DatabaseAdapter;
use super::ports::query::QueryBuilder;
use crate::domain::model::Claims;

#[derive(Clone)]
pub struct JWTRepository {
    adapter: Arc<dyn DatabaseAdapter<RefreshToken>>,
}
impl JWTRepository {
    pub fn new(adapter: Arc<dyn DatabaseAdapter<RefreshToken>>) -> Self {
        Self { adapter }
    }
}

impl JWTRepository {
    #[tracing::instrument(name = "db.jwt.insert", skip(self, refresh_token))]
    pub async fn insert(&self, refresh_token: RefreshToken) -> Result<()> {
        match self.adapter.insert(refresh_token).await {
            Ok(_id) => Ok(()),
            Err(err) => {
                tracing::error!("Failed to insert account to database - {err}");
                Err(err)
            }
        }
    }

    #[tracing::instrument(
        name = "db.jwt.find_and_consume", skip(self, claims), fields(user.id = claims.sub)
    )]
    pub async fn find_and_consume(&self, claims: &Claims) -> Result<RefreshToken> {
        let filter = QueryBuilder::default()
            .eq("jti", claims.jti.to_string())
            .eq("userId", claims.clone().sub)
            .gt("expiresAt", Utc::now())
            .is_null("usedAt");

        let update = QueryBuilder::default().set("usedAt", Utc::now());

        match self.adapter.find_one_and_update(filter, update).await {
            Ok(Some(token)) => Ok(token),
            Ok(None) => Err(CoreError::Unauthenticated(AuthError::TokenInvalid {
                token_type: TokenErrorType::RefreshToken,
            })),
            Err(err) => {
                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
                Err(err)
            }
        }
    }

    #[tracing::instrument(name = "db.jwt.find_by_jti", skip(self, jti))]
    pub async fn find_by_jti(&self, jti: &str) -> Result<RefreshToken> {
        let filter = QueryBuilder::default().eq("jti", jti);

        match self.adapter.find_one(filter).await {
            Ok(Some(token)) => Ok(token),
            Ok(None) => Err(CoreError::Unauthenticated(AuthError::TokenInvalid {
                token_type: TokenErrorType::RefreshToken,
            })),
            Err(err) => {
                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
                Err(err)
            }
        }
    }

    #[tracing::instrument(name = "db.jwt.invalidate", skip(self, jti))]
    pub async fn invalidate(&self, jti: uuid::Uuid) -> Result<RefreshToken> {
        let filter = QueryBuilder::default().eq("jti", jti.to_string());
        let update = QueryBuilder::default().set("usedAt", Utc::now());

        match self.adapter.find_one_and_update(filter, update).await {
            Ok(Some(refresh_token)) => Ok(refresh_token),
            Ok(None) => Err(CoreError::Unauthenticated(AuthError::TokenInvalid {
                token_type: TokenErrorType::RefreshToken,
            })),
            Err(err) => {
                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
                Err(err)
            }
        }
    }

    #[tracing::instrument(name = "db.jwt.revoke", skip(self), fields(user.id = user_id))]
    pub async fn revoke(&self, user_id: &str) -> Result<()> {
        let filter = QueryBuilder::default().eq("userId", user_id);
        let update = QueryBuilder::default().set("usedAt", Utc::now());

        self.adapter
            .update_many(filter, update)
            .await
            .inspect_err(|err| {
                tracing::error!(error_code = "InternalError::Database", error = %err, "Database query failed");
            })?;

        Ok(())
    }
}