prisma_auth 0.2.1

A simple authentication crate supporting both Rocket backend API and Yew frontend authentication components
Documentation
// src/backend.rs
use rocket::request::{FromRequest, Outcome, Request};
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{Duration, Instant};
use uuid::Uuid;

use crate::{LoginRequest, TokenResponse, AuthError, DEFAULT_TOKEN_TTL_SECONDS};

pub fn get_token_ttl() -> Duration {
    let ttl_seconds = std::env::var("TOKEN_TTL_SECONDS")
        .unwrap_or_else(|_| DEFAULT_TOKEN_TTL_SECONDS.to_string())
        .parse::<u64>()
        .unwrap_or(DEFAULT_TOKEN_TTL_SECONDS);
    Duration::from_secs(ttl_seconds)
}

/// Token storage for managing authentication tokens
pub struct TokenStore(RwLock<HashMap<String, Instant>>);

impl TokenStore {
    pub fn new() -> Self {
        TokenStore(RwLock::new(HashMap::new()))
    }

    /// Insert a token with current timestamp
    pub fn insert(&self, token: String) {
        self.0.write().unwrap().insert(token, Instant::now());
    }

    /// Check if a token is valid and not expired
    pub fn validate(&self, token: &str) -> bool {
        let mut map = self.0.write().unwrap();
        if let Some(&t0) = map.get(token) {
            if t0.elapsed() <= get_token_ttl() {
                return true;
            } else {
                // Token expired, remove it
                map.remove(token);
            }
        }
        false
    }

    /// Remove expired tokens
    pub fn cleanup_expired(&self) {
        let mut map = self.0.write().unwrap();
        let ttl = get_token_ttl();
        map.retain(|_, &mut timestamp| timestamp.elapsed() <= ttl);
    }
}

impl Default for TokenStore {
    fn default() -> Self {
        Self::new()
    }
}

/// Return a token on successful authentication
pub fn login_handler(body: Json<LoginRequest>, store: &State<TokenStore>) -> Result<Json<TokenResponse>, Status> {
    let expected = std::env::var("ADMIN_PASSWORD").unwrap_or_default();
    if body.password == expected {
        let token = Uuid::new_v4().to_string();
        store.insert(token.clone());
        Ok(Json(TokenResponse {
            token,
            username: None,
            role: None,
        }))
    } else {
        Err(Status::Unauthorized)
    }
}

/// Used for Rocket routes
pub struct AuthGuard;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthGuard {
    type Error = AuthError;

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, AuthError> {
        let store = match req.guard::<&State<TokenStore>>().await {
            Outcome::Success(store) => store,
            _ => return Outcome::Error((Status::InternalServerError, AuthError::StorageError("Token store not available".to_string()))),
        };

        match req.headers().get_one("Authorization") {
            Some(auth_header) => {
                // Handle both "Bearer <token>" and raw token formats
                let token = if auth_header.starts_with("Bearer ") {
                    &auth_header[7..]
                } else {
                    auth_header
                };
                
                if store.validate(token) {
                    Outcome::Success(AuthGuard)
                } else {
                    Outcome::Error((Status::Unauthorized, AuthError::TokenExpired))
                }
            },
            None => Outcome::Error((Status::Unauthorized, AuthError::TokenNotFound)),
        }
    }
}

/// Alias for backward compatibility
pub type Admin = AuthGuard;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_token_store() {
        let store = TokenStore::new();
        let token = "test-token".to_string();
        
        // Token should not be valid initially
        assert!(!store.validate(&token));
        
        // Insert token
        store.insert(token.clone());
        
        // Token should now be valid
        assert!(store.validate(&token));
    }

    #[test]
    fn test_token_ttl() {
        let ttl = get_token_ttl();
        assert_eq!(ttl, Duration::from_secs(DEFAULT_TOKEN_TTL_SECONDS));
    }
}