Fix new channels not being seen if your session hasn't expired yet
This commit is contained in:
1 parent
9e01b515e9
commit
39fdfd3cde
1 file changed
+91
-1
+91
-1
@@ -12,6 +12,7 @@ use rand::{Rng, distr::Alphanumeric};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer, cookie::time::Duration};
|
use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer, cookie::time::Duration};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ tokio::task_local! {
|
|||||||
|
|
||||||
const ITEMS_PER_PAGE: i64 = 100;
|
const ITEMS_PER_PAGE: i64 = 100;
|
||||||
const SHOWN_PAGES: i64 = 10;
|
const SHOWN_PAGES: i64 = 10;
|
||||||
|
const CHANNEL_ACCESS_TTL_SECONDS: u64 = 5 * 60;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct WebData {
|
struct WebData {
|
||||||
@@ -38,6 +40,8 @@ struct WebUser {
|
|||||||
username: String,
|
username: String,
|
||||||
channel_ids: Vec<i64>,
|
channel_ids: Vec<i64>,
|
||||||
channel_access: Vec<ChannelAccess>,
|
channel_access: Vec<ChannelAccess>,
|
||||||
|
#[serde(default)]
|
||||||
|
channel_access_refreshed_at: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
@@ -603,7 +607,26 @@ async fn require_user(
|
|||||||
next: Next,
|
next: Next,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match session.get::<WebUser>("user").await {
|
match session.get::<WebUser>("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 {
|
if !request_is_allowed(&data.pool, &user, request.uri().path()).await {
|
||||||
return StatusCode::NOT_FOUND.into_response();
|
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::<Vec<_>>()
|
||||||
|
.as_slice()
|
||||||
|
.get(0..2)
|
||||||
|
.and_then(|parts| match parts {
|
||||||
|
["channels", id] => id.parse::<i64>().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 {
|
async fn request_is_allowed(pool: &PgPool, user: &WebUser, path: &str) -> bool {
|
||||||
let parts = path.trim_matches('/').split('/').collect::<Vec<_>>();
|
let parts = path.trim_matches('/').split('/').collect::<Vec<_>>();
|
||||||
let channel_id = match parts.as_slice() {
|
let channel_id = match parts.as_slice() {
|
||||||
@@ -801,6 +852,7 @@ async fn discord_callback(
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
channel_ids,
|
channel_ids,
|
||||||
channel_access,
|
channel_access,
|
||||||
|
channel_access_refreshed_at: unix_timestamp(),
|
||||||
};
|
};
|
||||||
if session.cycle_id().await.is_err() {
|
if session.cycle_id().await.is_err() {
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||||
@@ -2637,6 +2689,44 @@ mod tests {
|
|||||||
assert_eq!(Theme::from_cookie(Some("unknown")).as_str(), "white");
|
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]
|
#[test]
|
||||||
fn shared_page_contains_the_footer_and_theme_switcher() {
|
fn shared_page_contains_the_footer_and_theme_switcher() {
|
||||||
let html = render_page("Archive", "Content", true);
|
let html = render_page("Archive", "Content", true);
|
||||||
|
|||||||
Reference in new issue
Block a user