feat: add Redis for OTP, auth tokens, rate limiting, lead dedup and marketplace cache
- Add crates/cache with client, otp, rate_limit, token, lead, jobs modules
- OTP tokens stored in Redis (15-min TTL, single-use GETDEL on verify)
- Refresh tokens stored in Redis (30-day TTL) — removed DB storage
- Password reset tokens stored in Redis (1-hour TTL, single-use)
- Rate limiting: register (10/hr), login (10/15min), OTP resend (3/hr), lead (5/hr), job post (20/hr)
- Lead request deduplication: 24-hour Redis lock per professional+requirement pair
- Marketplace listings cached in Redis (5-min TTL per profession+page+limit)
- Add ProfessionState{pool, redis} to contracts crate, replacing bare PgPool in all 9 profession apps
- All profession handlers and main.rs updated to use ProfessionState
- REDIS_URL env var (default: redis://127.0.0.1:6379) used across all services
- Fix profession model struct name mangling in 6 handlers (MakeupArtistRepository etc.)
- Add custom_data JSONB migration for all 9 profession profile tables
- Add onboarding_state model and repository (save_progress, complete, is_complete)
- Add onboarding handler accepting roleKey:String (not role_id:UUID) for frontend compat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:58:42 +01:00
|
|
|
|
//! 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 {
|
2026-03-22 15:55:29 +01:00
|
|
|
|
redis.expire::<_, ()>(&key, window_secs).await?;
|
feat: add Redis for OTP, auth tokens, rate limiting, lead dedup and marketplace cache
- Add crates/cache with client, otp, rate_limit, token, lead, jobs modules
- OTP tokens stored in Redis (15-min TTL, single-use GETDEL on verify)
- Refresh tokens stored in Redis (30-day TTL) — removed DB storage
- Password reset tokens stored in Redis (1-hour TTL, single-use)
- Rate limiting: register (10/hr), login (10/15min), OTP resend (3/hr), lead (5/hr), job post (20/hr)
- Lead request deduplication: 24-hour Redis lock per professional+requirement pair
- Marketplace listings cached in Redis (5-min TTL per profession+page+limit)
- Add ProfessionState{pool, redis} to contracts crate, replacing bare PgPool in all 9 profession apps
- All profession handlers and main.rs updated to use ProfessionState
- REDIS_URL env var (default: redis://127.0.0.1:6379) used across all services
- Fix profession model struct name mangling in 6 handlers (MakeupArtistRepository etc.)
- Add custom_data JSONB migration for all 9 profession profile tables
- Add onboarding_state model and repository (save_progress, complete, is_complete)
- Add onboarding handler accepting roleKey:String (not role_id:UUID) for frontend compat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:58:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
|
|
}
|