Argon2id password hashing, JWT access tokens with rotating HttpOnly refresh cookies and reuse detection, login rate limiting, auth audit log, bootstrap admin, admin-only user CRUD and password reset, OpenAPI spec. Vue login page, auth store with automatic token refresh, route guards, sidebar shell with toasts and placeholder pages, user management page. All tests green: 40 backend, 12 Vitest, 4 Playwright. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
44 lines
1.1 KiB
Rust
44 lines
1.1 KiB
Rust
//! Minimal fixed-window rate limiter keyed by client identifier (IP).
|
|
use std::collections::HashMap;
|
|
use std::sync::Mutex;
|
|
use std::time::{Duration, Instant};
|
|
|
|
pub struct RateLimiter {
|
|
max: u32,
|
|
window: Duration,
|
|
hits: Mutex<HashMap<String, (Instant, u32)>>,
|
|
}
|
|
|
|
impl RateLimiter {
|
|
pub fn new(max: u32, window: Duration) -> Self {
|
|
Self {
|
|
max,
|
|
window,
|
|
hits: Mutex::new(HashMap::new()),
|
|
}
|
|
}
|
|
|
|
/// Returns true if the request is allowed.
|
|
pub fn check(&self, key: &str) -> bool {
|
|
let mut hits = self.hits.lock().unwrap();
|
|
let now = Instant::now();
|
|
hits.retain(|_, (start, _)| now.duration_since(*start) < self.window);
|
|
let entry = hits.entry(key.to_string()).or_insert((now, 0));
|
|
entry.1 += 1;
|
|
entry.1 <= self.max
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn blocks_after_max_hits_per_key() {
|
|
let l = RateLimiter::new(3, Duration::from_secs(60));
|
|
assert!(l.check("a") && l.check("a") && l.check("a"));
|
|
assert!(!l.check("a"));
|
|
assert!(l.check("b"));
|
|
}
|
|
}
|