aurabase 0.1.1

Official Rust SDK for Aurabase: high-performance open-source Backend-as-a-Service (BaaS)
Documentation
use crate::types::User;
use base64::Engine;
use serde_json::Value;
use std::sync::RwLock;
use tokio::sync::broadcast;

/// Décode le payload d'un JWT sans vérifier sa signature (équivalent à atob en JS).
pub fn decode_jwt_payload(token: &str) -> Option<Value> {
    let parts: Vec<&str> = token.split('.').collect();
    if parts.len() != 3 {
        return None;
    }

    let mut payload = parts[1].to_string();
    // base64url -> base64
    payload = payload.replace('-', "+").replace('_', "/");
    while !payload.len().is_multiple_of(4) {
        payload.push('=');
    }

    let decoded = base64::engine::general_purpose::STANDARD
        .decode(payload)
        .ok()?;
    let json_str = String::from_utf8(decoded).ok()?;
    serde_json::from_str(&json_str).ok()
}

/// Vérifie si un token JWT est expiré (ou va expirer dans `margin_sec` secondes).
pub fn is_token_expired(token: &str, margin_sec: i64) -> bool {
    if let Some(payload) = decode_jwt_payload(token) {
        if let Some(exp) = payload.get("exp").and_then(|v| v.as_i64()) {
            let now = chrono::Utc::now().timestamp();
            return exp - margin_sec <= now;
        }
    }
    true
}

/// Trait représentant le stockage d'authentification.
pub trait AuthStore: Send + Sync + std::fmt::Debug {
    fn token(&self) -> Option<String>;
    fn refresh_token(&self) -> Option<String>;
    fn user(&self) -> Option<User>;
    fn is_valid(&self) -> bool;
    fn save(&self, token: String, refresh_token: String, user: User);
    fn clear(&self);
    fn subscribe(&self) -> broadcast::Receiver<(Option<String>, Option<User>)>;
}

/// Implémentation en mémoire par défaut du trait `AuthStore`.
#[derive(Debug)]
pub struct InMemoryAuthStore {
    token: RwLock<Option<String>>,
    refresh_token: RwLock<Option<String>>,
    user: RwLock<Option<User>>,
    tx: broadcast::Sender<(Option<String>, Option<User>)>,
}

impl Default for InMemoryAuthStore {
    fn default() -> Self {
        let (tx, _) = broadcast::channel(32);
        Self {
            token: RwLock::new(None),
            refresh_token: RwLock::new(None),
            user: RwLock::new(None),
            tx,
        }
    }
}

impl InMemoryAuthStore {
    pub fn new() -> Self {
        Self::default()
    }
}

impl AuthStore for InMemoryAuthStore {
    fn token(&self) -> Option<String> {
        self.token.read().unwrap().clone()
    }

    fn refresh_token(&self) -> Option<String> {
        self.refresh_token.read().unwrap().clone()
    }

    fn user(&self) -> Option<User> {
        self.user.read().unwrap().clone()
    }

    fn is_valid(&self) -> bool {
        if let Some(tok) = self.token() {
            !is_token_expired(&tok, 0)
        } else {
            false
        }
    }

    fn save(&self, token: String, refresh_token: String, user: User) {
        *self.token.write().unwrap() = Some(token.clone());
        *self.refresh_token.write().unwrap() = Some(refresh_token);
        *self.user.write().unwrap() = Some(user.clone());
        let _ = self.tx.send((Some(token), Some(user)));
    }

    fn clear(&self) {
        *self.token.write().unwrap() = None;
        *self.refresh_token.write().unwrap() = None;
        *self.user.write().unwrap() = None;
        let _ = self.tx.send((None, None));
    }

    fn subscribe(&self) -> broadcast::Receiver<(Option<String>, Option<User>)> {
        self.tx.subscribe()
    }
}