- 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>
33 lines
1 KiB
Rust
33 lines
1 KiB
Rust
//! Lead request deduplication.
|
|
//!
|
|
//! Prevents a professional from sending more than one lead request
|
|
//! to the same requirement within 24 hours.
|
|
//!
|
|
//! Key: `lead_dedup:{professional_id}:{requirement_id}` → "1", TTL 24 h
|
|
|
|
use redis::AsyncCommands;
|
|
use crate::RedisPool;
|
|
|
|
const DEDUP_TTL: u64 = 24 * 3_600; // 24 hours
|
|
|
|
/// Returns `true` if the professional has already sent a lead request
|
|
/// for this requirement in the last 24 hours.
|
|
pub async fn is_duplicate(
|
|
redis: &mut RedisPool,
|
|
professional_id: &str,
|
|
requirement_id: &str,
|
|
) -> Result<bool, redis::RedisError> {
|
|
let key = format!("lead_dedup:{professional_id}:{requirement_id}");
|
|
let exists: bool = redis.exists(key).await?;
|
|
Ok(exists)
|
|
}
|
|
|
|
/// Mark a lead request as sent. Call after successfully creating it in the DB.
|
|
pub async fn mark_sent(
|
|
redis: &mut RedisPool,
|
|
professional_id: &str,
|
|
requirement_id: &str,
|
|
) -> Result<(), redis::RedisError> {
|
|
let key = format!("lead_dedup:{professional_id}:{requirement_id}");
|
|
redis.set_ex(key, "1", DEDUP_TTL).await
|
|
}
|