All checks were successful
build-and-release / build (makeup-artists) (push) Successful in 8m13s
build-and-release / build (cron) (push) Successful in 4m39s
build-and-release / build (gateway) (push) Successful in 2m47s
build-and-release / build (employees) (push) Successful in 8m49s
build-and-release / build (fitness-trainers) (push) Successful in 7m42s
build-and-release / build (payments) (push) Successful in 8m6s
build-and-release / build (jobs) (push) Successful in 4m19s
build-and-release / build (catering-services) (push) Successful in 9m29s
build-and-release / build (photographers) (push) Successful in 8m44s
build-and-release / build (customers) (push) Successful in 9m30s
build-and-release / build (tutors) (push) Successful in 7m8s
build-and-release / build (developers) (push) Successful in 9m35s
build-and-release / build (graphic-designers) (push) Successful in 9m33s
build-and-release / build (social-media-managers) (push) Successful in 8m57s
build-and-release / build (leads) (push) Successful in 9m9s
build-and-release / build (ugc-content-creators) (push) Successful in 8m52s
build-and-release / build (video-editors) (push) Successful in 8m14s
build-and-release / build (companies) (push) Successful in 9m58s
build-and-release / build (job-seekers) (push) Successful in 9m55s
build-and-release / build (users) (push) Successful in 10m21s
The admin/employee login handler had no brute-force protection, unlike the regular user login path. Given these accounts hold internal/ super-admin privileges, add a tighter limit (5 attempts/15min vs 10 for regular users) using the existing sliding-window Redis limiter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
58 lines
2.2 KiB
Rust
58 lines
2.2 KiB
Rust
//! Generic sliding-window rate limiter.
|
||
//!
|
||
//! Key pattern: `rate:{namespace}:{identifier}`
|
||
//! Returns `Ok(true)` if the request is allowed, `Ok(false)` if rate-limited.
|
||
|
||
use redis::AsyncCommands;
|
||
use crate::RedisPool;
|
||
|
||
/// Check + increment a rate-limit counter.
|
||
///
|
||
/// * `namespace` – e.g. `"login"`, `"register"`, `"lead"`
|
||
/// * `identifier` – e.g. email, IP, user_id
|
||
/// * `max` – maximum requests allowed in `window_secs`
|
||
/// * `window_secs` – sliding window length in seconds
|
||
///
|
||
/// Returns `Ok(true)` = allowed, `Ok(false)` = blocked.
|
||
pub async fn check(
|
||
redis: &mut RedisPool,
|
||
namespace: &str,
|
||
identifier: &str,
|
||
max: i64,
|
||
window_secs: i64,
|
||
) -> Result<bool, redis::RedisError> {
|
||
let key = format!("rate:{namespace}:{identifier}");
|
||
let count: i64 = redis.incr(&key, 1i64).await?;
|
||
if count == 1 {
|
||
redis.expire::<_, ()>(&key, window_secs).await?;
|
||
}
|
||
Ok(count <= max)
|
||
}
|
||
|
||
/// Convenience wrappers ───────────────────────────────────────────────────────
|
||
|
||
/// Register: max 10 per hour per email
|
||
pub async fn check_register(redis: &mut RedisPool, email: &str) -> Result<bool, redis::RedisError> {
|
||
check(redis, "register", email, 10, 3_600).await
|
||
}
|
||
|
||
/// Login: max 10 attempts per 15 min per email
|
||
pub async fn check_login(redis: &mut RedisPool, email: &str) -> Result<bool, redis::RedisError> {
|
||
check(redis, "login", email, 10, 900).await
|
||
}
|
||
|
||
/// Admin/employee login: max 5 attempts per 15 min per email (tighter than regular
|
||
/// user login because these accounts hold internal/super-admin privileges).
|
||
pub async fn check_admin_login(redis: &mut RedisPool, email: &str) -> Result<bool, redis::RedisError> {
|
||
check(redis, "admin_login", email, 5, 900).await
|
||
}
|
||
|
||
/// Lead request: max 5 per hour per professional
|
||
pub async fn check_lead(redis: &mut RedisPool, professional_id: &str) -> Result<bool, redis::RedisError> {
|
||
check(redis, "lead", professional_id, 5, 3_600).await
|
||
}
|
||
|
||
/// Job post: max 20 per hour per company
|
||
pub async fn check_job_post(redis: &mut RedisPool, company_id: &str) -> Result<bool, redis::RedisError> {
|
||
check(redis, "job_post", company_id, 20, 3_600).await
|
||
}
|