From 39fdfd3cde8eedec41388c3360e61b45efa048e5 Mon Sep 17 00:00:00 2001 From: Elias Wendland <193786789+eliaswen@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:40:16 +0200 Subject: [PATCH] Fix new channels not being seen if your session hasn't expired yet --- src/web.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/src/web.rs b/src/web.rs index 6074eac..f973563 100644 --- a/src/web.rs +++ b/src/web.rs @@ -12,6 +12,7 @@ use rand::{Rng, distr::Alphanumeric}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::collections::{HashMap, HashSet}; +use std::time::{SystemTime, UNIX_EPOCH}; use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer, cookie::time::Duration}; use tracing::{error, info}; @@ -21,6 +22,7 @@ tokio::task_local! { const ITEMS_PER_PAGE: i64 = 100; const SHOWN_PAGES: i64 = 10; +const CHANNEL_ACCESS_TTL_SECONDS: u64 = 5 * 60; #[derive(Clone)] struct WebData { @@ -38,6 +40,8 @@ struct WebUser { username: String, channel_ids: Vec, channel_access: Vec, + #[serde(default)] + channel_access_refreshed_at: u64, } #[derive(Clone, Deserialize, Serialize)] @@ -603,7 +607,26 @@ async fn require_user( next: Next, ) -> Response { match session.get::("user").await { - Ok(Some(user)) => { + Ok(Some(mut user)) => { + if channel_access_needs_refresh(&user, request.uri().path()) { + let channel_access = match accessible_channels(&data, user.id).await { + Ok(channel_access) => channel_access, + Err(error) => { + error!("Discord permission refresh failed: {}", error); + return StatusCode::BAD_GATEWAY.into_response(); + } + }; + user.channel_ids = channel_access + .iter() + .map(|access| access.channel_id) + .collect(); + user.channel_access = channel_access; + user.channel_access_refreshed_at = unix_timestamp(); + if let Err(error) = session.insert("user", &user).await { + error!("Could not update refreshed session permissions: {}", error); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + } if !request_is_allowed(&data.pool, &user, request.uri().path()).await { return StatusCode::NOT_FOUND.into_response(); } @@ -618,6 +641,34 @@ async fn require_user( } } +fn unix_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +fn channel_access_needs_refresh_at(user: &WebUser, path: &str, now: u64) -> bool { + let expired = + now.saturating_sub(user.channel_access_refreshed_at) >= CHANNEL_ACCESS_TTL_SECONDS; + let requested_channel_is_missing = path + .trim_matches('/') + .split('/') + .collect::>() + .as_slice() + .get(0..2) + .and_then(|parts| match parts { + ["channels", id] => id.parse::().ok(), + _ => None, + }) + .is_some_and(|channel_id| !user.channel_ids.contains(&channel_id)); + expired || requested_channel_is_missing +} + +fn channel_access_needs_refresh(user: &WebUser, path: &str) -> bool { + channel_access_needs_refresh_at(user, path, unix_timestamp()) +} + async fn request_is_allowed(pool: &PgPool, user: &WebUser, path: &str) -> bool { let parts = path.trim_matches('/').split('/').collect::>(); let channel_id = match parts.as_slice() { @@ -801,6 +852,7 @@ async fn discord_callback( username: user.username, channel_ids, channel_access, + channel_access_refreshed_at: unix_timestamp(), }; if session.cycle_id().await.is_err() { return StatusCode::INTERNAL_SERVER_ERROR.into_response(); @@ -2637,6 +2689,44 @@ mod tests { assert_eq!(Theme::from_cookie(Some("unknown")).as_str(), "white"); } + #[test] + fn refreshes_expired_or_legacy_channel_access() { + let mut user = WebUser { + id: 1, + username: "user".into(), + channel_ids: vec![10], + channel_access: Vec::new(), + channel_access_refreshed_at: 1_000, + }; + + assert!(!channel_access_needs_refresh_at(&user, "/channels", 1_299)); + assert!(channel_access_needs_refresh_at(&user, "/channels", 1_300)); + user.channel_access_refreshed_at = 0; + assert!(channel_access_needs_refresh_at(&user, "/channels", 1_000)); + } + + #[test] + fn refreshes_when_a_directly_requested_channel_is_not_cached() { + let user = WebUser { + id: 1, + username: "user".into(), + channel_ids: vec![10], + channel_access: Vec::new(), + channel_access_refreshed_at: 1_000, + }; + + assert!(!channel_access_needs_refresh_at( + &user, + "/channels/10/messages", + 1_001 + )); + assert!(channel_access_needs_refresh_at( + &user, + "/channels/20/messages", + 1_001 + )); + } + #[test] fn shared_page_contains_the_footer_and_theme_switcher() { let html = render_page("Archive", "Content", true);