Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/scrybe-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
pub mod client;
/// Nonce validation for replay attack prevention.
pub mod nonce;
/// Rate limiting with token bucket algorithm.
pub mod rate_limit;
/// Session cache management.
pub mod session;

// Re-export main types
pub use client::RedisClient;
pub use nonce::NonceValidator;
pub use rate_limit::RateLimiter;
pub use session::SessionCache;
116 changes: 116 additions & 0 deletions crates/scrybe-cache/src/rate_limit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! Rate limiting using Redis token bucket algorithm.

use crate::client::RedisClient;
use redis::AsyncCommands;
use scrybe_core::ScrybeError;

/// Redis-backed rate limiter using token bucket algorithm.
pub struct RateLimiter {
client: RedisClient,
max_requests: usize,
window_seconds: usize,
}

impl RateLimiter {
/// Create a new rate limiter.
///
/// # Arguments
///
/// * `client` - Redis client instance
/// * `max_requests` - Maximum requests allowed in the window
/// * `window_seconds` - Time window in seconds
///
/// # Example
///
/// ```no_run
/// # use scrybe_cache::{RedisClient, RateLimiter};
/// # async fn example() -> Result<(), scrybe_core::ScrybeError> {
/// let client = RedisClient::new("redis://localhost", 10).await?;
/// let limiter = RateLimiter::new(client, 100, 60); // 100 requests per minute
/// # Ok(())
/// # }
/// ```
pub fn new(client: RedisClient, max_requests: usize, window_seconds: usize) -> Self {
Self {
client,
max_requests,
window_seconds,
}
}

/// Check if a request is allowed for the given identifier.
///
/// Returns `true` if the request is allowed, `false` if rate limit exceeded.
///
/// # Arguments
///
/// * `identifier` - Unique identifier (e.g., IP address, session ID)
///
/// # Errors
///
/// Returns `ScrybeError::CacheError` if Redis operation fails.
pub async fn check(&self, identifier: &str) -> Result<bool, ScrybeError> {
let key = format!("ratelimit:{}", identifier);

let mut conn = self.client.get_connection().await?;

// Increment counter
let count: usize = conn
.incr(&key, 1)
.await
.map_err(|e| ScrybeError::cache_error("redis", format!("INCR failed: {}", e)))?;

// Set expiration on first request
if count == 1 {
conn.expire::<_, ()>(&key, self.window_seconds as i64)
.await
.map_err(|e| ScrybeError::cache_error("redis", format!("EXPIRE failed: {}", e)))?;
}

Ok(count <= self.max_requests)
}

/// Get current request count for an identifier.
///
/// # Errors
///
/// Returns `ScrybeError::CacheError` if Redis operation fails.
pub async fn get_count(&self, identifier: &str) -> Result<usize, ScrybeError> {
let key = format!("ratelimit:{}", identifier);

let mut conn = self.client.get_connection().await?;

let count: Option<usize> = conn
.get(&key)
.await
.map_err(|e| ScrybeError::cache_error("redis", format!("GET failed: {}", e)))?;

Ok(count.unwrap_or(0))
}

/// Reset rate limit for an identifier.
///
/// # Errors
///
/// Returns `ScrybeError::CacheError` if Redis operation fails.
pub async fn reset(&self, identifier: &str) -> Result<(), ScrybeError> {
let key = format!("ratelimit:{}", identifier);

let mut conn = self.client.get_connection().await?;

conn.del::<_, ()>(&key)
.await
.map_err(|e| ScrybeError::cache_error("redis", format!("DEL failed: {}", e)))?;

Ok(())
}
}

#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_rate_limiter_compiles() {
// Placeholder test
assert!(true);
}
}
89 changes: 80 additions & 9 deletions crates/scrybe-cache/src/session.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
//! Session cache management with Redis.

use crate::client::RedisClient;
use redis::AsyncCommands;
use scrybe_core::{
types::{Session, SessionId},
ScrybeError,
};

/// Redis-backed session cache.
pub struct SessionCache;
/// Redis-backed session cache with TTL.
///
/// Sessions are stored for 1 hour (3600 seconds) to minimize memory usage.
pub struct SessionCache {
client: RedisClient,
ttl_seconds: usize,
}

impl SessionCache {
/// Create a new session cache.
///
/// # Arguments
///
/// * `client` - Redis client instance
/// * `ttl_seconds` - Time-to-live for sessions (default: 3600 = 1 hour)
pub fn new(client: RedisClient, ttl_seconds: Option<usize>) -> Self {
Self {
client,
ttl_seconds: ttl_seconds.unwrap_or(3600),
}
}

/// Store a session in the cache with TTL.
///
/// # Errors
///
/// Returns `ScrybeError::CacheError` if the operation fails.
pub async fn store(_session: &Session) -> Result<(), ScrybeError> {
// TODO: Implement Redis storage
pub async fn store(&self, session: &Session) -> Result<(), ScrybeError> {
let key = format!("session:{}", session.id);
let value = serde_json::to_string(session).map_err(|e| {
ScrybeError::cache_error("redis", format!("Serialization failed: {}", e))
})?;

let mut conn = self.client.get_connection().await?;

conn.set_ex::<_, _, ()>(&key, &value, self.ttl_seconds as u64)
.await
.map_err(|e| ScrybeError::cache_error("redis", format!("SET failed: {}", e)))?;

Ok(())
}

Expand All @@ -24,20 +54,61 @@ impl SessionCache {
/// # Errors
///
/// Returns `ScrybeError::CacheError` if the operation fails.
pub async fn get(_session_id: &SessionId) -> Result<Option<Session>, ScrybeError> {
// TODO: Implement Redis retrieval
Ok(None)
pub async fn get(&self, session_id: &SessionId) -> Result<Option<Session>, ScrybeError> {
let key = format!("session:{}", session_id);

let mut conn = self.client.get_connection().await?;

let value: Option<String> = conn
.get(&key)
.await
.map_err(|e| ScrybeError::cache_error("redis", format!("GET failed: {}", e)))?;

match value {
Some(json) => {
let session = serde_json::from_str(&json).map_err(|e| {
ScrybeError::cache_error("redis", format!("Deserialization failed: {}", e))
})?;
Ok(Some(session))
}
None => Ok(None),
}
}

/// Delete a session from the cache.
///
/// # Errors
///
/// Returns `ScrybeError::CacheError` if the operation fails.
pub async fn delete(_session_id: &SessionId) -> Result<(), ScrybeError> {
// TODO: Implement Redis deletion
pub async fn delete(&self, session_id: &SessionId) -> Result<(), ScrybeError> {
let key = format!("session:{}", session_id);

let mut conn = self.client.get_connection().await?;

conn.del::<_, ()>(&key)
.await
.map_err(|e| ScrybeError::cache_error("redis", format!("DEL failed: {}", e)))?;

Ok(())
}

/// Check if a session exists in the cache.
///
/// # Errors
///
/// Returns `ScrybeError::CacheError` if the operation fails.
pub async fn exists(&self, session_id: &SessionId) -> Result<bool, ScrybeError> {
let key = format!("session:{}", session_id);

let mut conn = self.client.get_connection().await?;

let exists: bool = conn
.exists(&key)
.await
.map_err(|e| ScrybeError::cache_error("redis", format!("EXISTS failed: {}", e)))?;

Ok(exists)
}
}

#[cfg(test)]
Expand Down