All checks were successful
build-and-release / build (cron) (push) Successful in 4m55s
build-and-release / build (catering-services) (push) Successful in 9m18s
build-and-release / build (developers) (push) Successful in 9m14s
build-and-release / build (employees) (push) Successful in 9m15s
build-and-release / build (customers) (push) Successful in 9m35s
build-and-release / build (companies) (push) Successful in 9m49s
build-and-release / build (gateway) (push) Successful in 3m2s
build-and-release / build (fitness-trainers) (push) Successful in 8m20s
build-and-release / build (jobs) (push) Successful in 4m22s
build-and-release / build (graphic-designers) (push) Successful in 8m41s
build-and-release / build (job-seekers) (push) Successful in 9m14s
build-and-release / build (leads) (push) Successful in 9m10s
build-and-release / build (makeup-artists) (push) Successful in 8m51s
build-and-release / build (payments) (push) Successful in 8m51s
build-and-release / build (photographers) (push) Successful in 8m39s
build-and-release / build (ugc-content-creators) (push) Successful in 6m57s
build-and-release / build (social-media-managers) (push) Successful in 8m25s
build-and-release / build (tutors) (push) Successful in 8m28s
build-and-release / build (video-editors) (push) Successful in 8m43s
build-and-release / build (users) (push) Successful in 11m8s
Employees (internal admin/staff) had no self-service password reset — only /login, /logout, /session existed. Adds /api/admin/auth/forgot-password and /api/admin/auth/reset-password, mirroring the existing users-table flow but against EmployeeRepository and a distinct Redis key namespace (reset:employee:*) so a code for one identity store can never be consumed against the other. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
89 lines
3 KiB
Rust
89 lines
3 KiB
Rust
//! Auth token management in Redis.
|
|
//!
|
|
//! Refresh tokens
|
|
//! ──────────────
|
|
//! `refresh:{token}` → user_id, TTL 30 days
|
|
//!
|
|
//! Password-reset tokens
|
|
//! ─────────────────────
|
|
//! `reset:{token}` → user_id, TTL 1 hour
|
|
|
|
use redis::AsyncCommands;
|
|
use crate::RedisPool;
|
|
|
|
const REFRESH_TTL: u64 = 30 * 24 * 3_600; // 30 days in seconds
|
|
const RESET_TTL: u64 = 900; // 15 minutes
|
|
|
|
// ── Refresh tokens ────────────────────────────────────────────────────────────
|
|
|
|
pub async fn store_refresh(
|
|
redis: &mut RedisPool,
|
|
token: &str,
|
|
user_id: &str,
|
|
) -> Result<(), redis::RedisError> {
|
|
let key = format!("refresh:{token}");
|
|
redis.set_ex(key, user_id, REFRESH_TTL).await
|
|
}
|
|
|
|
/// Returns the user_id string associated with this token, or `None` if expired/invalid.
|
|
pub async fn get_refresh(
|
|
redis: &mut RedisPool,
|
|
token: &str,
|
|
) -> Result<Option<String>, redis::RedisError> {
|
|
let key = format!("refresh:{token}");
|
|
redis.get(key).await
|
|
}
|
|
|
|
/// Delete refresh token (logout / rotation).
|
|
pub async fn revoke_refresh(
|
|
redis: &mut RedisPool,
|
|
token: &str,
|
|
) -> Result<(), redis::RedisError> {
|
|
let key = format!("refresh:{token}");
|
|
redis.del(key).await
|
|
}
|
|
|
|
// ── Password-reset tokens ─────────────────────────────────────────────────────
|
|
|
|
pub async fn store_reset(
|
|
redis: &mut RedisPool,
|
|
token: &str,
|
|
user_id: &str,
|
|
) -> Result<(), redis::RedisError> {
|
|
let key = format!("reset:{token}");
|
|
let plain_key = format!("otp:plain:{user_id}");
|
|
// Store both: token→user_id (for verification) and plain→token (for dev debugging)
|
|
redis.set_ex::<_, _, ()>(&plain_key, token, RESET_TTL).await?;
|
|
redis.set_ex::<_, _, ()>(key, user_id, RESET_TTL).await
|
|
}
|
|
|
|
/// Atomically fetch and delete the reset token (single-use).
|
|
pub async fn consume_reset(
|
|
redis: &mut RedisPool,
|
|
token: &str,
|
|
) -> Result<Option<String>, redis::RedisError> {
|
|
let key = format!("reset:{token}");
|
|
redis.get_del(key).await
|
|
}
|
|
|
|
// ── Employee (internal admin) password-reset tokens ──────────────────────────
|
|
// Distinct key namespace from the user-facing reset tokens above, so a code
|
|
// generated for one identity store can never be consumed against the other.
|
|
|
|
pub async fn store_employee_reset(
|
|
redis: &mut RedisPool,
|
|
token: &str,
|
|
employee_id: &str,
|
|
) -> Result<(), redis::RedisError> {
|
|
let key = format!("reset:employee:{token}");
|
|
redis.set_ex(key, employee_id, RESET_TTL).await
|
|
}
|
|
|
|
/// Atomically fetch and delete the employee reset token (single-use).
|
|
pub async fn consume_employee_reset(
|
|
redis: &mut RedisPool,
|
|
token: &str,
|
|
) -> Result<Option<String>, redis::RedisError> {
|
|
let key = format!("reset:employee:{token}");
|
|
redis.get_del(key).await
|
|
}
|