use rustlavel::prelude::*;
use std::time::Duration;
use crate::models::user::User;
pub const MAX_ATTEMPTS: i64 = 5;
pub const LOCK_FOR: Duration = Duration::from_secs(15 * 60);
pub const MAX_PER_ADDRESS: u64 = 20;
pub const ADDRESS_WINDOW: Duration = Duration::from_secs(15 * 60);
pub async fn address_is_blocked(req: &Request) -> bool {
let Some(cache) = req.state::<CacheStore>() else { return false };
let key = format!("login-failures:{}", req.ip().unwrap_or_else(|| "unknown".into()));
matches!(
cache.driver_handle().get(&key).await,
Ok(Some(Json::Number(count))) if count as u64 >= MAX_PER_ADDRESS
)
}
pub async fn record_address_failure(req: &Request) {
let Some(cache) = req.state::<CacheStore>() else { return };
let key = format!("login-failures:{}", req.ip().unwrap_or_else(|| "unknown".into()));
let _ = cache.driver_handle().increment_within(&key, 1, ADDRESS_WINDOW).await;
}
pub async fn record_failure(db: &Database, user: &mut User, now: &str) -> Result<bool> {
user.failed_attempts += 1;
if user.failed_attempts >= MAX_ATTEMPTS {
user.locked_until = Some(crate::support::tokens::in_future(now, LOCK_FOR));
user.update(db).await?;
return Ok(true);
}
user.update(db).await?;
Ok(false)
}
pub async fn record_success(db: &Database, user: &mut User, req: &Request, now: &str) -> Result<()> {
user.failed_attempts = 0;
user.locked_until = None;
user.last_login_at = Some(now.to_string());
user.last_login_ip = req.ip();
user.update(db).await?;
if let Some(cache) = req.state::<CacheStore>() {
let key = format!("login-failures:{}", req.ip().unwrap_or_else(|| "unknown".into()));
let _ = cache.driver_handle().forget(&key).await;
}
Ok(())
}
pub fn remaining(locked_until: &str, now: &str) -> String {
let minutes = crate::support::tokens::minutes_between(now, locked_until);
match minutes {
0 => "less than a minute".to_string(),
1 => "1 minute".to_string(),
n => format!("{n} minutes"),
}
}