From 39332670be0c32a3c5293a99439505671857f4cd Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 13 Oct 2026 00:20:58 +0530 Subject: [PATCH] Enforce 30-day session expiry with lazy cleanup Sessions never expired server-side despite the 30-day cookie Max-Age; repeat sign-ins orphaned rows. user_id now rejects tokens older than 30 days and deletes them on sight. Verified live: fresh 200, backdated 302 with row removed. --- src/auth.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 7a50101..4384c6c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -100,17 +100,32 @@ pub async fn user_id(pool: &SqlitePool, h: &HeaderMap) -> Option { } let token = token?; tracing::debug!("session token={token}"); - match sqlx::query_scalar::<_, String>("SELECT user_id FROM sessions WHERE token=?") - .bind(token) - .fetch_optional(pool) - .await - { - Ok(found) => { - tracing::debug!("session lookup -> {:?}", found); - found + // Sessions live 30 days (matching the cookie Max-Age). Stale rows are + // deleted on sight so repeat sign-ins don't orphan tokens forever. + let row: Option<(String, String)> = + sqlx::query_as("SELECT user_id, created_at FROM sessions WHERE token=?") + .bind(token) + .fetch_optional(pool) + .await + .unwrap_or(None); + match row { + Some((uid, created)) => { + let fresh = chrono::DateTime::parse_from_rfc3339(&created) + .map(|t| (chrono::Utc::now() - t.with_timezone(&chrono::Utc)).num_days() < 30) + .unwrap_or(false); + if !fresh { + sqlx::query("DELETE FROM sessions WHERE token=?") + .bind(token) + .execute(pool) + .await + .ok(); + return None; + } + tracing::debug!("session lookup -> {:?}", uid); + Some(uid) } - Err(e) => { - tracing::error!("session lookup failed: {e}"); + None => { + tracing::debug!("session lookup -> None"); None } }