From b718aa33128d520e1bb6099615bee482a0ef6e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 21 Nov 2025 09:37:55 +0100 Subject: [PATCH 01/52] pass peer stats from gRPC server to session manager --- crates/defguard/src/main.rs | 19 +- crates/defguard_core/src/grpc/gateway/mod.rs | 183 ++++++++++--------- crates/defguard_core/src/grpc/mod.rs | 9 +- 3 files changed, 114 insertions(+), 97 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 087ac62bdf..50efb54838 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -10,10 +10,8 @@ use defguard_common::{ db::{ init_db, models::{ - Settings, - User, - settings::initialize_current_settings, - // wireguard_peer_stats::WireguardPeerStats, + Settings, User, settings::initialize_current_settings, + wireguard_peer_stats::WireguardPeerStats, }, }, }; @@ -40,7 +38,7 @@ use defguard_core::{ use defguard_event_logger::{message::EventLoggerMessage, run_event_logger}; use defguard_event_router::{RouterReceiverSet, run_event_router}; use defguard_mail::{Mail, run_mail_handler}; -// use defguard_session_manager::run_session_manager; +use defguard_session_manager::run_session_manager; use secrecy::ExposeSecret; use tokio::sync::{broadcast, mpsc::unbounded_channel}; @@ -112,7 +110,7 @@ async fn main() -> Result<(), anyhow::Error> { let (wireguard_tx, _wireguard_rx) = broadcast::channel::(256); let (mail_tx, mail_rx) = unbounded_channel::(); let (event_logger_tx, event_logger_rx) = unbounded_channel::(); - // let (peer_stats_tx, peer_stats_rx) = unbounded_channel::(); + let (peer_stats_tx, peer_stats_rx) = unbounded_channel::(); let worker_state = Arc::new(Mutex::new(WorkerState::new(webhook_tx.clone()))); let gateway_state = Arc::new(Mutex::new(GatewayMap::new())); @@ -179,6 +177,7 @@ async fn main() -> Result<(), anyhow::Error> { failed_logins.clone(), grpc_event_tx, Arc::clone(&incompatible_components), + peer_stats_tx, ) => error!("gRPC server returned early: {res:?}"), res = run_web_server( worker_state, @@ -227,10 +226,10 @@ async fn main() -> Result<(), anyhow::Error> { activity_log_stream_reload_notify.clone(), activity_log_messages_rx ) => error!("Activity log stream manager returned early: {res:?}"), - // res = run_session_manager( - // pool.clone(), - // peer_stats_rx - // ) => error!("VPN client session manager returned early: {res:?}"), + res = run_session_manager( + pool.clone(), + peer_stats_rx + ) => error!("VPN client session manager returned early: {res:?}"), } Ok(()) diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 73a2afd730..9862293a89 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -120,6 +120,7 @@ pub struct GatewayServer { wireguard_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, } /// If this location is marked as a service location, checks if all requirements are met for it to function: @@ -147,6 +148,7 @@ impl GatewayServer { wireguard_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, ) -> Self { Self { pool, @@ -155,6 +157,7 @@ impl GatewayServer { wireguard_tx, mail_tx, grpc_event_tx, + peer_stats_tx, } } @@ -792,97 +795,105 @@ impl gateway_service_server::GatewayService for GatewayServer { // convert stats to DB storage format let stats = protos_into_internal_stats(peer_stats, network_id, device_id); + self.peer_stats_tx.send(stats).map_err(|err| { + error!("Failed to send peers stats to session manager: {err}"); + Status::new( + Code::Internal, + format!("Failed to send peers stats to session manager: {err}"), + ) + })?; + // only perform client state update if stats include an endpoint IP // otherwise a peer was added to the gateway interface // but has not connected yet - if let Some(endpoint) = &stats.endpoint { - // parse client endpoint IP - let socket_addr: SocketAddr = endpoint.clone().parse().map_err(|err| { - error!("Failed to parse VPN client endpoint: {err}"); - Status::new( - Code::Internal, - format!("Failed to parse VPN client endpoint: {err}"), - ) - })?; - - // perform client state operations in a dedicated block to drop mutex guard - let disconnected_clients = { - // acquire lock on client state map - let mut client_map = self.get_client_state_guard()?; - - // update connected clients map - match client_map.get_vpn_client(network_id, &public_key) { - Some(client_state) => { - // update connected client state - client_state.update_client_state( - device, - socket_addr, - stats.latest_handshake, - stats.upload, - stats.download, - ); - } - None => { - // don't mark inactive peers as connected - if (Utc::now().naive_utc() - stats.latest_handshake) - < TimeDelta::seconds(location.peer_disconnect_threshold.into()) - { - // mark new VPN client as connected - client_map.connect_vpn_client( - network_id, - &hostname, - &public_key, - &device, - &user, - socket_addr, - &stats, - )?; - - // emit connection event - let context = GrpcRequestContext::new( - user.id, - user.username.clone(), - socket_addr.ip(), - device.id, - device.name.clone(), - location.clone(), - ); - self.emit_event(GrpcEvent::ClientConnected { - context, - location: location.clone(), - device: device.clone(), - })?; - } - } - } - - // disconnect inactive clients - client_map.disconnect_inactive_vpn_clients_for_location(&location)? - }; - - // emit client disconnect events - for (device, context) in disconnected_clients { - self.emit_event(GrpcEvent::ClientDisconnected { - context, - location: location.clone(), - device, - })?; - } - } + // if let Some(endpoint) = &stats.endpoint { + // // parse client endpoint IP + // let socket_addr: SocketAddr = endpoint.clone().parse().map_err(|err| { + // error!("Failed to parse VPN client endpoint: {err}"); + // Status::new( + // Code::Internal, + // format!("Failed to parse VPN client endpoint: {err}"), + // ) + // })?; + + // // perform client state operations in a dedicated block to drop mutex guard + // let disconnected_clients = { + // // acquire lock on client state map + // let mut client_map = self.get_client_state_guard()?; + + // // update connected clients map + // match client_map.get_vpn_client(network_id, &public_key) { + // Some(client_state) => { + // // update connected client state + // client_state.update_client_state( + // device, + // socket_addr, + // stats.latest_handshake, + // stats.upload, + // stats.download, + // ); + // } + // None => { + // // don't mark inactive peers as connected + // if (Utc::now().naive_utc() - stats.latest_handshake) + // < TimeDelta::seconds(location.peer_disconnect_threshold.into()) + // { + // // mark new VPN client as connected + // client_map.connect_vpn_client( + // network_id, + // &hostname, + // &public_key, + // &device, + // &user, + // socket_addr, + // &stats, + // )?; + + // // emit connection event + // let context = GrpcRequestContext::new( + // user.id, + // user.username.clone(), + // socket_addr.ip(), + // device.id, + // device.name.clone(), + // location.clone(), + // ); + // self.emit_event(GrpcEvent::ClientConnected { + // context, + // location: location.clone(), + // device: device.clone(), + // })?; + // } + // } + // } + + // // disconnect inactive clients + // client_map.disconnect_inactive_vpn_clients_for_location(&location)? + // }; + + // // emit client disconnect events + // for (device, context) in disconnected_clients { + // self.emit_event(GrpcEvent::ClientDisconnected { + // context, + // location: location.clone(), + // device, + // })?; + // } + // } // Save stats to db - let stats = match stats.save(&self.pool).await { - Ok(stats) => stats, - Err(err) => { - error!("Saving WireGuard peer stats to db failed: {err}"); - return Err(Status::new( - Code::Internal, - format!("Saving WireGuard peer stats to db failed: {err}"), - )); - } - }; - info!("Saved WireGuard peer stats to db."); - debug!("WireGuard peer stats: {stats:?}"); + // let stats = match stats.save(&self.pool).await { + // Ok(stats) => stats, + // Err(err) => { + // error!("Saving WireGuard peer stats to db failed: {err}"); + // return Err(Status::new( + // Code::Internal, + // format!("Saving WireGuard peer stats to db failed: {err}"), + // )); + // } + // }; + // info!("Saved WireGuard peer stats to db."); + // debug!("WireGuard peer stats: {stats:?}"); } Ok(Response::new(())) diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index 90ef3c681c..65498dea1f 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -10,7 +10,10 @@ use axum::http::Uri; use defguard_common::{ VERSION, auth::claims::ClaimsType, - db::{Id, models::Settings}, + db::{ + Id, + models::{Settings, wireguard_peer_stats::WireguardPeerStats}, + }, }; use defguard_mail::Mail; use defguard_version::{ @@ -666,6 +669,7 @@ pub async fn run_grpc_server( failed_logins: Arc>, grpc_event_tx: UnboundedSender, incompatible_components: Arc>, + peer_stats_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { // Build gRPC services let server = if let (Some(cert), Some(key)) = (grpc_cert, grpc_key) { @@ -686,6 +690,7 @@ pub async fn run_grpc_server( failed_logins, grpc_event_tx, incompatible_components, + peer_stats_tx, ) .await?; @@ -713,6 +718,7 @@ pub async fn build_grpc_service_router( failed_logins: Arc>, grpc_event_tx: UnboundedSender, incompatible_components: Arc>, + peer_stats_tx: UnboundedSender, ) -> Result { let auth_service = AuthServiceServer::new(AuthServer::new(pool.clone(), failed_logins)); @@ -742,6 +748,7 @@ pub async fn build_grpc_service_router( wireguard_tx, mail_tx, grpc_event_tx, + peer_stats_tx, )); let own_version = Version::parse(VERSION)?; From d40b7edca29612ea36289f10664056d7102889c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 21 Nov 2025 11:02:06 +0100 Subject: [PATCH 02/52] add a dedicated type for stats update message --- Cargo.lock | 1 + crates/defguard/src/main.rs | 8 +- crates/defguard_common/src/lib.rs | 1 + crates/defguard_common/src/messages/mod.rs | 1 + .../src/messages/peer_stats_update.rs | 41 +++ crates/defguard_core/src/grpc/gateway/mod.rs | 242 ++++++++++-------- crates/defguard_core/src/grpc/mod.rs | 5 +- crates/defguard_session_manager/Cargo.toml | 1 + crates/defguard_session_manager/src/lib.rs | 34 ++- 9 files changed, 221 insertions(+), 113 deletions(-) create mode 100644 crates/defguard_common/src/messages/mod.rs create mode 100644 crates/defguard_common/src/messages/peer_stats_update.rs diff --git a/Cargo.lock b/Cargo.lock index c87a0e3c3d..d47ab45f59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1287,6 +1287,7 @@ dependencies = [ "defguard_common", "sqlx", "tokio", + "tracing", ] [[package]] diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 50efb54838..fcced3db39 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -9,11 +9,9 @@ use defguard_common::{ config::{Command, DefGuardConfig, SERVER_CONFIG}, db::{ init_db, - models::{ - Settings, User, settings::initialize_current_settings, - wireguard_peer_stats::WireguardPeerStats, - }, + models::{Settings, User, settings::initialize_current_settings}, }, + messages::peer_stats_update::PeerStatsUpdate, }; use defguard_core::{ auth::failed_login::FailedLoginMap, @@ -110,7 +108,7 @@ async fn main() -> Result<(), anyhow::Error> { let (wireguard_tx, _wireguard_rx) = broadcast::channel::(256); let (mail_tx, mail_rx) = unbounded_channel::(); let (event_logger_tx, event_logger_rx) = unbounded_channel::(); - let (peer_stats_tx, peer_stats_rx) = unbounded_channel::(); + let (peer_stats_tx, peer_stats_rx) = unbounded_channel::(); let worker_state = Arc::new(Mutex::new(WorkerState::new(webhook_tx.clone()))); let gateway_state = Arc::new(Mutex::new(GatewayMap::new())); diff --git a/crates/defguard_common/src/lib.rs b/crates/defguard_common/src/lib.rs index fb2351648d..20e6ec8e66 100644 --- a/crates/defguard_common/src/lib.rs +++ b/crates/defguard_common/src/lib.rs @@ -4,6 +4,7 @@ pub mod csv; pub mod db; pub mod globals; pub mod hex; +pub mod messages; pub mod random; pub mod secret; pub mod types; diff --git a/crates/defguard_common/src/messages/mod.rs b/crates/defguard_common/src/messages/mod.rs new file mode 100644 index 0000000000..8aacc75f2a --- /dev/null +++ b/crates/defguard_common/src/messages/mod.rs @@ -0,0 +1 @@ +pub mod peer_stats_update; diff --git a/crates/defguard_common/src/messages/peer_stats_update.rs b/crates/defguard_common/src/messages/peer_stats_update.rs new file mode 100644 index 0000000000..8ad3b87c87 --- /dev/null +++ b/crates/defguard_common/src/messages/peer_stats_update.rs @@ -0,0 +1,41 @@ +use std::net::SocketAddr; + +use chrono::{NaiveDateTime, Utc}; + +use crate::db::Id; + +/// Represents stats read from a WireGuard interface +/// sent from a gateway +pub struct PeerStatsUpdate { + location_id: Id, + device_id: Id, + collected_at: NaiveDateTime, + endpoint: SocketAddr, + // bytes sent to peer + upload: u64, + // bytes received from peer + download: u64, + latest_handshake: NaiveDateTime, +} + +impl PeerStatsUpdate { + pub fn new( + location_id: Id, + device_id: Id, + endpoint: SocketAddr, + upload: u64, + download: u64, + latest_handshake: NaiveDateTime, + ) -> Self { + let collected_at = Utc::now().naive_utc(); + Self { + location_id, + device_id, + collected_at, + endpoint, + upload, + download, + latest_handshake, + } + } +} diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 9862293a89..12e177644a 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -7,12 +7,15 @@ use std::{ use chrono::{DateTime, TimeDelta, Utc}; use client_state::ClientMap; -use defguard_common::db::{ - Id, NoId, - models::{ - Device, User, WireguardNetwork, wireguard::ServiceLocationMode, - wireguard_peer_stats::WireguardPeerStats, +use defguard_common::{ + db::{ + Id, NoId, + models::{ + Device, User, WireguardNetwork, wireguard::ServiceLocationMode, + wireguard_peer_stats::WireguardPeerStats, + }, }, + messages::peer_stats_update::PeerStatsUpdate, }; use defguard_mail::Mail; use defguard_proto::{ @@ -98,6 +101,33 @@ fn protos_into_internal_stats( } } +/// Helper used to convert peer stats coming from gRPC client +/// into an internal representation +fn try_protos_into_stats_message( + proto_stats: PeerStats, + location_id: Id, + device_id: Id, +) -> Option { + let endpoint = todo!(); + // try to parse endpoint + // let endpoint = match proto_stats.endpoint { + // endpoint if endpoint.is_empty() => None, + // _ => Some(proto_stats.endpoint), + // }; + let latest_handshake = DateTime::from_timestamp(proto_stats.latest_handshake as i64, 0) + .unwrap_or_default() + .naive_utc(); + + Some(PeerStatsUpdate::new( + location_id, + device_id, + endpoint, + proto_stats.upload, + proto_stats.download, + latest_handshake, + )) +} + #[allow(clippy::large_enum_variant)] #[derive(Debug, Error)] pub enum GatewayServerError { @@ -120,7 +150,7 @@ pub struct GatewayServer { wireguard_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, - peer_stats_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, } /// If this location is marked as a service location, checks if all requirements are met for it to function: @@ -148,7 +178,7 @@ impl GatewayServer { wireguard_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, - peer_stats_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, ) -> Self { Self { pool, @@ -793,107 +823,117 @@ impl gateway_service_server::GatewayService for GatewayServer { let location = self.fetch_location_from_db(network_id).await?; // convert stats to DB storage format - let stats = protos_into_internal_stats(peer_stats, network_id, device_id); + match try_protos_into_stats_message(peer_stats.clone(), network_id, device_id) { + None => { + warn!( + "Failed to parse peer stats update. Skipping sending message to session manager." + ) + } + Some(message) => { + self.peer_stats_tx.send(message).map_err(|err| { + error!("Failed to send peers stats update to session manager: {err}"); + Status::new( + Code::Internal, + format!("Failed to send peers stats update to session manager: {err}"), + ) + })?; + } + }; - self.peer_stats_tx.send(stats).map_err(|err| { - error!("Failed to send peers stats to session manager: {err}"); - Status::new( - Code::Internal, - format!("Failed to send peers stats to session manager: {err}"), - ) - })?; + // convert stats to DB storage format + let stats = protos_into_internal_stats(peer_stats, network_id, device_id); // only perform client state update if stats include an endpoint IP // otherwise a peer was added to the gateway interface // but has not connected yet - // if let Some(endpoint) = &stats.endpoint { - // // parse client endpoint IP - // let socket_addr: SocketAddr = endpoint.clone().parse().map_err(|err| { - // error!("Failed to parse VPN client endpoint: {err}"); - // Status::new( - // Code::Internal, - // format!("Failed to parse VPN client endpoint: {err}"), - // ) - // })?; - - // // perform client state operations in a dedicated block to drop mutex guard - // let disconnected_clients = { - // // acquire lock on client state map - // let mut client_map = self.get_client_state_guard()?; - - // // update connected clients map - // match client_map.get_vpn_client(network_id, &public_key) { - // Some(client_state) => { - // // update connected client state - // client_state.update_client_state( - // device, - // socket_addr, - // stats.latest_handshake, - // stats.upload, - // stats.download, - // ); - // } - // None => { - // // don't mark inactive peers as connected - // if (Utc::now().naive_utc() - stats.latest_handshake) - // < TimeDelta::seconds(location.peer_disconnect_threshold.into()) - // { - // // mark new VPN client as connected - // client_map.connect_vpn_client( - // network_id, - // &hostname, - // &public_key, - // &device, - // &user, - // socket_addr, - // &stats, - // )?; - - // // emit connection event - // let context = GrpcRequestContext::new( - // user.id, - // user.username.clone(), - // socket_addr.ip(), - // device.id, - // device.name.clone(), - // location.clone(), - // ); - // self.emit_event(GrpcEvent::ClientConnected { - // context, - // location: location.clone(), - // device: device.clone(), - // })?; - // } - // } - // } - - // // disconnect inactive clients - // client_map.disconnect_inactive_vpn_clients_for_location(&location)? - // }; - - // // emit client disconnect events - // for (device, context) in disconnected_clients { - // self.emit_event(GrpcEvent::ClientDisconnected { - // context, - // location: location.clone(), - // device, - // })?; - // } - // } + if let Some(endpoint) = &stats.endpoint { + // parse client endpoint IP + let socket_addr: SocketAddr = endpoint.clone().parse().map_err(|err| { + error!("Failed to parse VPN client endpoint: {err}"); + Status::new( + Code::Internal, + format!("Failed to parse VPN client endpoint: {err}"), + ) + })?; + + // perform client state operations in a dedicated block to drop mutex guard + let disconnected_clients = { + // acquire lock on client state map + let mut client_map = self.get_client_state_guard()?; + + // update connected clients map + match client_map.get_vpn_client(network_id, &public_key) { + Some(client_state) => { + // update connected client state + client_state.update_client_state( + device, + socket_addr, + stats.latest_handshake, + stats.upload, + stats.download, + ); + } + None => { + // don't mark inactive peers as connected + if (Utc::now().naive_utc() - stats.latest_handshake) + < TimeDelta::seconds(location.peer_disconnect_threshold.into()) + { + // mark new VPN client as connected + client_map.connect_vpn_client( + network_id, + &hostname, + &public_key, + &device, + &user, + socket_addr, + &stats, + )?; + + // emit connection event + let context = GrpcRequestContext::new( + user.id, + user.username.clone(), + socket_addr.ip(), + device.id, + device.name.clone(), + location.clone(), + ); + self.emit_event(GrpcEvent::ClientConnected { + context, + location: location.clone(), + device: device.clone(), + })?; + } + } + } + + // disconnect inactive clients + client_map.disconnect_inactive_vpn_clients_for_location(&location)? + }; + + // emit client disconnect events + for (device, context) in disconnected_clients { + self.emit_event(GrpcEvent::ClientDisconnected { + context, + location: location.clone(), + device, + })?; + } + } // Save stats to db - // let stats = match stats.save(&self.pool).await { - // Ok(stats) => stats, - // Err(err) => { - // error!("Saving WireGuard peer stats to db failed: {err}"); - // return Err(Status::new( - // Code::Internal, - // format!("Saving WireGuard peer stats to db failed: {err}"), - // )); - // } - // }; - // info!("Saved WireGuard peer stats to db."); - // debug!("WireGuard peer stats: {stats:?}"); + let stats = match stats.save(&self.pool).await { + Ok(stats) => stats, + Err(err) => { + error!("Saving WireGuard peer stats to db failed: {err}"); + return Err(Status::new( + Code::Internal, + format!("Saving WireGuard peer stats to db failed: {err}"), + )); + } + }; + info!("Saved WireGuard peer stats to db."); + debug!("WireGuard peer stats: {stats:?}"); } Ok(Response::new(())) diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index 65498dea1f..9afb5bd4e3 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -14,6 +14,7 @@ use defguard_common::{ Id, models::{Settings, wireguard_peer_stats::WireguardPeerStats}, }, + messages::peer_stats_update::PeerStatsUpdate, }; use defguard_mail::Mail; use defguard_version::{ @@ -669,7 +670,7 @@ pub async fn run_grpc_server( failed_logins: Arc>, grpc_event_tx: UnboundedSender, incompatible_components: Arc>, - peer_stats_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { // Build gRPC services let server = if let (Some(cert), Some(key)) = (grpc_cert, grpc_key) { @@ -718,7 +719,7 @@ pub async fn build_grpc_service_router( failed_logins: Arc>, grpc_event_tx: UnboundedSender, incompatible_components: Arc>, - peer_stats_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, ) -> Result { let auth_service = AuthServiceServer::new(AuthServer::new(pool.clone(), failed_logins)); diff --git a/crates/defguard_session_manager/Cargo.toml b/crates/defguard_session_manager/Cargo.toml index be683e13f0..33894fde86 100644 --- a/crates/defguard_session_manager/Cargo.toml +++ b/crates/defguard_session_manager/Cargo.toml @@ -11,4 +11,5 @@ rust-version.workspace = true defguard_common.workspace = true sqlx.workspace = true tokio.workspace = true +tracing.workspace = true diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index beb3865559..5d0f00b3e3 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -1,10 +1,34 @@ -use defguard_common::db::models::wireguard_peer_stats::WireguardPeerStats; +use defguard_common::messages::peer_stats_update::PeerStatsUpdate; use sqlx::PgPool; -use tokio::sync::mpsc::UnboundedReceiver; +use tokio::{ + sync::mpsc::UnboundedReceiver, + time::{Duration, interval}, +}; +use tracing::{debug, info}; + +const MESSAGE_LIMIT: usize = 100; +const SESSION_UPDATE_INTERVAL: u64 = 60; pub async fn run_session_manager( - _pool: PgPool, - _peer_stats_rx: UnboundedReceiver, + pool: PgPool, + mut peer_stats_rx: UnboundedReceiver, ) { - unimplemented!() + info!("Starting VPN client session manager service"); + let mut session_update_timer = interval(Duration::from_secs(SESSION_UPDATE_INTERVAL)); + + loop { + // receive next batch of peer stats messages + // if no message is received within `SESSION_UPDATE_INTERVAL` trigger session status refresh anyway + let mut message_buffer: Vec = Vec::with_capacity(MESSAGE_LIMIT); + let message_count = tokio::select! { + message_count = peer_stats_rx.recv_many(&mut message_buffer, MESSAGE_LIMIT) => message_count, + _ = session_update_timer.tick() => { + debug!("No wireguard peer stats updates received in last {SESSION_UPDATE_INTERVAL}. Triggering session status update."); + continue; + } + + }; + + debug!("Processing batch of {message_count} peer stats updates"); + } } From 835e8fd25cacb46ed5194686f1e1ae8f2099f805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 21 Nov 2025 11:59:34 +0100 Subject: [PATCH 03/52] sketch out basic logic of the session manager --- Cargo.lock | 1 + .../src/messages/peer_stats_update.rs | 14 ++--- crates/defguard_session_manager/Cargo.toml | 1 + crates/defguard_session_manager/src/error.rs | 7 +++ crates/defguard_session_manager/src/lib.rs | 44 +++++++++++++++- .../src/session_state.rs | 51 +++++++++++++++++++ 6 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 crates/defguard_session_manager/src/error.rs create mode 100644 crates/defguard_session_manager/src/session_state.rs diff --git a/Cargo.lock b/Cargo.lock index d47ab45f59..f881b4bf47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1286,6 +1286,7 @@ version = "0.0.0" dependencies = [ "defguard_common", "sqlx", + "thiserror 2.0.17", "tokio", "tracing", ] diff --git a/crates/defguard_common/src/messages/peer_stats_update.rs b/crates/defguard_common/src/messages/peer_stats_update.rs index 8ad3b87c87..4ec89b70e3 100644 --- a/crates/defguard_common/src/messages/peer_stats_update.rs +++ b/crates/defguard_common/src/messages/peer_stats_update.rs @@ -7,15 +7,15 @@ use crate::db::Id; /// Represents stats read from a WireGuard interface /// sent from a gateway pub struct PeerStatsUpdate { - location_id: Id, - device_id: Id, - collected_at: NaiveDateTime, - endpoint: SocketAddr, + pub location_id: Id, + pub device_id: Id, + pub collected_at: NaiveDateTime, + pub endpoint: SocketAddr, // bytes sent to peer - upload: u64, + pub upload: u64, // bytes received from peer - download: u64, - latest_handshake: NaiveDateTime, + pub download: u64, + pub latest_handshake: NaiveDateTime, } impl PeerStatsUpdate { diff --git a/crates/defguard_session_manager/Cargo.toml b/crates/defguard_session_manager/Cargo.toml index 33894fde86..acd7ebfbe0 100644 --- a/crates/defguard_session_manager/Cargo.toml +++ b/crates/defguard_session_manager/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true [dependencies] defguard_common.workspace = true sqlx.workspace = true +thiserror.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/crates/defguard_session_manager/src/error.rs b/crates/defguard_session_manager/src/error.rs new file mode 100644 index 0000000000..90e8bab933 --- /dev/null +++ b/crates/defguard_session_manager/src/error.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SessionManagerError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), +} diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 5d0f00b3e3..cf19be0d98 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use defguard_common::messages::peer_stats_update::PeerStatsUpdate; use sqlx::PgPool; use tokio::{ @@ -6,29 +8,67 @@ use tokio::{ }; use tracing::{debug, info}; +use crate::{error::SessionManagerError, session_state::LocationSessionsMap}; + +pub mod error; +pub mod session_state; + const MESSAGE_LIMIT: usize = 100; const SESSION_UPDATE_INTERVAL: u64 = 60; pub async fn run_session_manager( pool: PgPool, mut peer_stats_rx: UnboundedReceiver, -) { +) -> Result<(), SessionManagerError> { info!("Starting VPN client session manager service"); let mut session_update_timer = interval(Duration::from_secs(SESSION_UPDATE_INTERVAL)); + // initialize active sessions state based on DB content + let mut active_sessions = LocationSessionsMap::initialize_from_db(&pool).await?; + loop { // receive next batch of peer stats messages // if no message is received within `SESSION_UPDATE_INTERVAL` trigger session status refresh anyway + // to disconnect inactive sessions if necessary let mut message_buffer: Vec = Vec::with_capacity(MESSAGE_LIMIT); let message_count = tokio::select! { message_count = peer_stats_rx.recv_many(&mut message_buffer, MESSAGE_LIMIT) => message_count, _ = session_update_timer.tick() => { - debug!("No wireguard peer stats updates received in last {SESSION_UPDATE_INTERVAL}. Triggering session status update."); + info!("No wireguard peer stats updates received in last {SESSION_UPDATE_INTERVAL}. Triggering session status update."); + active_sessions.update_session_status(); continue; } }; debug!("Processing batch of {message_count} peer stats updates"); + + // create temporary maps of DB objects to avoid repeated queries + // let location_map = HashMap::new(); + // let user_map = HashMap::new(); + // let device_map = HashMap::new(); + + // begin DB transaction + let transaction = pool.begin().await?; + + for message in message_buffer { + // check if a session exists already for a given peer + match active_sessions.try_get_peer_session() { + Some(mut session) => { + // session exists already, update it based on received stats + session.update(message); + } + None => { + debug!( + "No active session found for device {} in location {}. Creating a new session", + message.device_id, message.location_id + ); + active_sessions.new_session(); + } + } + } + + // commit DB transaction after processing all messages + transaction.commit().await?; } } diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs new file mode 100644 index 0000000000..5702f97f13 --- /dev/null +++ b/crates/defguard_session_manager/src/session_state.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; + +use defguard_common::{db::Id, messages::peer_stats_update::PeerStatsUpdate}; +use sqlx::PgPool; +use tracing::debug; + +use crate::error::SessionManagerError; + +/// State of a specific VPN client session +pub(crate) struct SessionState {} + +impl SessionState { + /// Updates session state based on received peer update + pub(crate) fn update(&mut self, peer_stats_update: PeerStatsUpdate) { + todo!() + } +} + +/// Represents all active sessions for a given location +pub(crate) struct SessionMap(HashMap); + +/// Helper struct to hold session maps for all locations +pub(crate) struct LocationSessionsMap(HashMap); + +impl LocationSessionsMap { + /// Fetch current active sessions for all locations from DB + /// and initialize session map + pub(crate) async fn initialize_from_db(pool: &PgPool) -> Result { + debug!("Initializing active sessions map from DB"); + todo!() + } + + /// Checks if a session for a given peer exists already + pub(crate) fn try_get_peer_session(&self) -> Option { + todo!() + } + + pub(crate) fn get_location_sessions(&self, location_id: Id) { + todo!() + } + + /// Checks if any sessions need to be marked as disconnected + pub(crate) fn update_session_status(&mut self) { + todo!() + } + + /// Creates a new VPN client session, adds it to curent state and persists it in DB + pub(crate) fn new_session(&mut self) { + todo!() + } +} From 16ac3d4e731302338cfe2c1bc39be484fad75c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 21 Nov 2025 13:41:54 +0100 Subject: [PATCH 04/52] setup basic session models --- crates/defguard_common/src/db/models/mod.rs | 2 ++ .../src/db/models/vpn_client_session.rs | 19 +++++++++++++++++++ .../src/db/models/vpn_session_stats.rs | 8 ++++++++ ...0251121121935_vpn_client_sessions.down.sql | 2 ++ .../20251121121935_vpn_client_sessions.up.sql | 19 +++++++++++++++++++ 5 files changed, 50 insertions(+) create mode 100644 crates/defguard_common/src/db/models/vpn_client_session.rs create mode 100644 crates/defguard_common/src/db/models/vpn_session_stats.rs create mode 100644 migrations/20251121121935_vpn_client_sessions.down.sql create mode 100644 migrations/20251121121935_vpn_client_sessions.up.sql diff --git a/crates/defguard_common/src/db/models/mod.rs b/crates/defguard_common/src/db/models/mod.rs index 653e62d2ab..ce14005a3b 100644 --- a/crates/defguard_common/src/db/models/mod.rs +++ b/crates/defguard_common/src/db/models/mod.rs @@ -13,6 +13,8 @@ pub mod polling_token; pub mod session; pub mod settings; pub mod user; +pub mod vpn_client_session; +pub mod vpn_session_stats; pub mod webauthn; pub mod wireguard; pub mod wireguard_peer_stats; diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs new file mode 100644 index 0000000000..3bd0f9107e --- /dev/null +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -0,0 +1,19 @@ +use chrono::NaiveDateTime; +use model_derive::Model; + +use crate::db::{Id, NoId}; + +/// Represents a single VPN client session from creation to eventual disconnection +#[derive(Model)] +#[table(vpn_client_session)] +pub struct VpnClientSession { + pub id: I, + pub location_id: Id, + pub user_id: Id, + // users can delete their device, but we want to retain sessions & stats + pub device_id: Option, + pub created_at: NaiveDateTime, + pub connected_at: NaiveDateTime, + pub disconnected_at: NaiveDateTime, + pub mfa: bool, +} diff --git a/crates/defguard_common/src/db/models/vpn_session_stats.rs b/crates/defguard_common/src/db/models/vpn_session_stats.rs new file mode 100644 index 0000000000..8220a37ff6 --- /dev/null +++ b/crates/defguard_common/src/db/models/vpn_session_stats.rs @@ -0,0 +1,8 @@ +use crate::db::{Id, NoId}; + +#[derive(Model)] +#[table(vpn_session_stats)] +pub struct VpnSessionStats { + pub id: I, + pub session_id: Id, +} diff --git a/migrations/20251121121935_vpn_client_sessions.down.sql b/migrations/20251121121935_vpn_client_sessions.down.sql new file mode 100644 index 0000000000..80625d35f9 --- /dev/null +++ b/migrations/20251121121935_vpn_client_sessions.down.sql @@ -0,0 +1,2 @@ +DROP TABLE vpn_session_stats; +DROP TABLE vpn_client_session; diff --git a/migrations/20251121121935_vpn_client_sessions.up.sql b/migrations/20251121121935_vpn_client_sessions.up.sql new file mode 100644 index 0000000000..7be9b2f1b2 --- /dev/null +++ b/migrations/20251121121935_vpn_client_sessions.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE vpn_client_session ( + id bigserial PRIMARY KEY, + location_id bigint NOT NULL, + user_id bigint NOT NULL, + device_id bigint NULL, + created_at timestamp without time zone NOT NULL DEFAULT current_timestamp, + connected_at timestamp without time zone NOT NULL, + disconnected_at timestamp without time zone NOT NULL, + mfa boolean NOT NULL, + FOREIGN KEY (location_id) REFERENCES wireguard_network(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES "user"(id) ON DELETE CASCADE, + FOREIGN KEY (device_id) REFERENCES device(id) ON DELETE SET NULL +); + +CREATE TABLE vpn_session_stats ( + id bigserial PRIMARY KEY, + session_id bigint NOT NULL, + FOREIGN KEY (session_id) REFERENCES vpn_client_session(id) ON DELETE CASCADE +); From 6e06d15fb09ad5a61261d3e8cac73302bf613535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 21 Nov 2025 14:00:41 +0100 Subject: [PATCH 05/52] add session state enum --- .../src/db/models/vpn_client_session.rs | 10 ++++++++++ .../defguard_common/src/db/models/vpn_session_stats.rs | 6 ++++++ migrations/20251121121935_vpn_client_sessions.down.sql | 1 + migrations/20251121121935_vpn_client_sessions.up.sql | 9 +++++++++ 4 files changed, 26 insertions(+) diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index 3bd0f9107e..1b704f68b0 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -1,8 +1,17 @@ use chrono::NaiveDateTime; use model_derive::Model; +use sqlx::Type; use crate::db::{Id, NoId}; +#[derive(Type)] +#[sqlx(type_name = "vpn_client_session_state", rename_all = "lowercase")] +pub enum VpnClientSessionState { + New, + Connected, + Disconnected, +} + /// Represents a single VPN client session from creation to eventual disconnection #[derive(Model)] #[table(vpn_client_session)] @@ -16,4 +25,5 @@ pub struct VpnClientSession { pub connected_at: NaiveDateTime, pub disconnected_at: NaiveDateTime, pub mfa: bool, + pub state: VpnClientSessionState, } diff --git a/crates/defguard_common/src/db/models/vpn_session_stats.rs b/crates/defguard_common/src/db/models/vpn_session_stats.rs index 8220a37ff6..cdbd6b7c9d 100644 --- a/crates/defguard_common/src/db/models/vpn_session_stats.rs +++ b/crates/defguard_common/src/db/models/vpn_session_stats.rs @@ -1,3 +1,5 @@ +use model_derive::Model; + use crate::db::{Id, NoId}; #[derive(Model)] @@ -5,4 +7,8 @@ use crate::db::{Id, NoId}; pub struct VpnSessionStats { pub id: I, pub session_id: Id, + // uplad since last stats update + pub upload_diff: i64, + // download since last stats update + pub download_diff: i64, } diff --git a/migrations/20251121121935_vpn_client_sessions.down.sql b/migrations/20251121121935_vpn_client_sessions.down.sql index 80625d35f9..8e23639f41 100644 --- a/migrations/20251121121935_vpn_client_sessions.down.sql +++ b/migrations/20251121121935_vpn_client_sessions.down.sql @@ -1,2 +1,3 @@ DROP TABLE vpn_session_stats; DROP TABLE vpn_client_session; +DROP TYPE vpn_client_session_state; diff --git a/migrations/20251121121935_vpn_client_sessions.up.sql b/migrations/20251121121935_vpn_client_sessions.up.sql index 7be9b2f1b2..e47bda8934 100644 --- a/migrations/20251121121935_vpn_client_sessions.up.sql +++ b/migrations/20251121121935_vpn_client_sessions.up.sql @@ -1,3 +1,9 @@ +CREATE TYPE vpn_client_session_state AS ENUM ( + 'new', + 'connected', + 'disconnected' +); + CREATE TABLE vpn_client_session ( id bigserial PRIMARY KEY, location_id bigint NOT NULL, @@ -7,6 +13,7 @@ CREATE TABLE vpn_client_session ( connected_at timestamp without time zone NOT NULL, disconnected_at timestamp without time zone NOT NULL, mfa boolean NOT NULL, + state vpn_client_session_state NOT NULL DEFAULT 'new', FOREIGN KEY (location_id) REFERENCES wireguard_network(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES "user"(id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES device(id) ON DELETE SET NULL @@ -15,5 +22,7 @@ CREATE TABLE vpn_client_session ( CREATE TABLE vpn_session_stats ( id bigserial PRIMARY KEY, session_id bigint NOT NULL, + upload_diff bigint NOT NULL, + download_diff bigint NOT NULL, FOREIGN KEY (session_id) REFERENCES vpn_client_session(id) ON DELETE CASCADE ); From f3f7f9de648cf86e44c152a50c3845b3dc160235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 8 Dec 2025 10:10:56 +0100 Subject: [PATCH 06/52] finish up session map initialization logic --- .../src/db/models/vpn_client_session.rs | 4 +- .../src/db/models/wireguard.rs | 18 +++ crates/defguard_session_manager/src/error.rs | 17 ++- .../src/session_state.rs | 105 +++++++++++++++++- 4 files changed, 138 insertions(+), 6 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index 1b704f68b0..54ec349205 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -4,9 +4,10 @@ use sqlx::Type; use crate::db::{Id, NoId}; -#[derive(Type)] +#[derive(Default, Type)] #[sqlx(type_name = "vpn_client_session_state", rename_all = "lowercase")] pub enum VpnClientSessionState { + #[default] New, Connected, Disconnected, @@ -25,5 +26,6 @@ pub struct VpnClientSession { pub connected_at: NaiveDateTime, pub disconnected_at: NaiveDateTime, pub mfa: bool, + #[model(enum)] pub state: VpnClientSessionState, } diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 04c4de1db3..74006b7fab 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -12,6 +12,7 @@ use crate::{ models::{ ModelError, group::{Group, Permission}, + vpn_client_session::{VpnClientSession, VpnClientSessionState}, wireguard_peer_stats::WireguardPeerStats, }, }, @@ -1036,6 +1037,23 @@ impl WireguardNetwork { ); Ok(()) } + + /// Fetch all active VPN client sessions + pub async fn get_active_vpn_sessions<'e, E: sqlx::PgExecutor<'e>>( + &self, + executor: E, + ) -> Result>, SqlxError> { + query_as!( + VpnClientSession, + "SELECT id, location_id, user_id, device_id, \ + created_at, connected_at, disconnected_at, mfa, state \"state: VpnClientSessionState\" \ + FROM vpn_client_session \ + WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", + self.id, + ) + .fetch_all(executor) + .await + } } // [`IpNetwork`] does not implement [`Default`] diff --git a/crates/defguard_session_manager/src/error.rs b/crates/defguard_session_manager/src/error.rs index 90e8bab933..c9bc5c4dd7 100644 --- a/crates/defguard_session_manager/src/error.rs +++ b/crates/defguard_session_manager/src/error.rs @@ -1,7 +1,22 @@ +use defguard_common::db::Id; use thiserror::Error; #[derive(Debug, Error)] pub enum SessionManagerError { #[error("Database error: {0}")] - Database(#[from] sqlx::Error), + DatabaseError(#[from] sqlx::Error), + #[error( + "Found multiple active sessions for user {username}, device {device_name} in location {location_name}" + )] + MultipleActiveSessionsError { + location_name: String, + username: String, + device_name: String, + }, + #[error("User with ID {0} does not exist")] + UserDoesNotExistError(Id), + #[error("Device with ID {0} does not exist")] + DeviceDoesNotExistError(Id), + #[error("Session map initialization error: {0}")] + SessionMapInitializationError(String), } diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index 5702f97f13..ba8d8b7fac 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -1,15 +1,34 @@ use std::collections::HashMap; -use defguard_common::{db::Id, messages::peer_stats_update::PeerStatsUpdate}; +use defguard_common::{ + db::{ + Id, + models::{Device, User, WireguardNetwork, vpn_client_session::VpnClientSession}, + }, + messages::peer_stats_update::PeerStatsUpdate, +}; use sqlx::PgPool; -use tracing::debug; +use tracing::{debug, error}; use crate::error::SessionManagerError; /// State of a specific VPN client session -pub(crate) struct SessionState {} +pub(crate) struct SessionState { + session_id: Id, + user_id: Id, + username: String, + last_stats_update: Option, +} impl SessionState { + fn new(session_id: Id, user: &User) -> Self { + Self { + session_id, + last_stats_update: None, + user_id: user.id, + username: user.username.clone(), + } + } /// Updates session state based on received peer update pub(crate) fn update(&mut self, peer_stats_update: PeerStatsUpdate) { todo!() @@ -19,15 +38,80 @@ impl SessionState { /// Represents all active sessions for a given location pub(crate) struct SessionMap(HashMap); +impl SessionMap { + /// Helper to insert into inner map + fn insert(&mut self, key: Id, session_state: SessionState) -> Option { + self.0.insert(key, session_state) + } +} + /// Helper struct to hold session maps for all locations pub(crate) struct LocationSessionsMap(HashMap); +impl LocationSessionsMap { + /// Helper to insert into inner map + fn insert(&mut self, key: Id, session_map: SessionMap) -> Option { + self.0.insert(key, session_map) + } +} + impl LocationSessionsMap { /// Fetch current active sessions for all locations from DB /// and initialize session map pub(crate) async fn initialize_from_db(pool: &PgPool) -> Result { debug!("Initializing active sessions map from DB"); - todo!() + + // initialize empty map + let mut active_sessions = LocationSessionsMap(HashMap::new()); + + // fetch all locations + let locations = WireguardNetwork::all(pool).await?; + + // get active sessions for all locations + for location in locations { + // fetch active sessions from DB + let location_sessions = location.get_active_vpn_sessions(pool).await?; + + // initialize empty session map for a given location + let mut location_session_map = SessionMap(HashMap::new()); + + // insert sessions into map + for session in location_sessions { + // we can unwrap here since active session must have a device ID + let device_id = session + .device_id + .expect("Active session must have device_id"); + + let device = Self::fetch_device(pool, device_id).await?; + + let user = Self::fetch_user(pool, session.user_id).await?; + + let session_state = SessionState::new(session.id, &user); + + if let Some(existing_session) = + location_session_map.insert(device_id, session_state) + { + error!( + "Found duplicate active session for device {device} in location {location}" + ); + return Err(SessionManagerError::MultipleActiveSessionsError { + location_name: location.name, + username: existing_session.username, + device_name: device.name, + }); + }; + } + + if let Some(_) = active_sessions.insert(location.id, location_session_map) { + let msg = format!( + "Active sessions for location {location} have already been initialized" + ); + error!("{msg}"); + return Err(SessionManagerError::SessionMapInitializationError(msg)); + }; + } + + Ok(active_sessions) } /// Checks if a session for a given peer exists already @@ -48,4 +132,17 @@ impl LocationSessionsMap { pub(crate) fn new_session(&mut self) { todo!() } + + // Wrapper method which attempts to fetch User from DB and returns an error if None is found or an error occurs + async fn fetch_user(pool: &PgPool, user_id: Id) -> Result, SessionManagerError> { + User::find_by_id(pool, user_id) + .await? + .ok_or(SessionManagerError::UserDoesNotExistError(user_id)) + } + // Wrapper method which attempts to fetch Device from DB and returns an error if None is found or an error occurs + async fn fetch_device(pool: &PgPool, device_id: Id) -> Result, SessionManagerError> { + Device::find_by_id(pool, device_id) + .await? + .ok_or(SessionManagerError::DeviceDoesNotExistError(device_id)) + } } From 3c0ecc07edeb780cab32906acfdda41e395753f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 12 Dec 2025 10:06:12 +0100 Subject: [PATCH 07/52] WIP: adding new sessions --- .../src/db/models/vpn_client_session.rs | 35 ++++- .../src/messages/peer_stats_update.rs | 1 + crates/defguard_session_manager/src/error.rs | 2 + crates/defguard_session_manager/src/lib.rs | 122 +++++++++++++----- .../src/session_state.rs | 69 ++++++++-- flake.lock | 12 +- 6 files changed, 193 insertions(+), 48 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index 54ec349205..e9ad5ba48d 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -1,4 +1,4 @@ -use chrono::NaiveDateTime; +use chrono::{NaiveDateTime, Utc}; use model_derive::Model; use sqlx::Type; @@ -23,9 +23,38 @@ pub struct VpnClientSession { // users can delete their device, but we want to retain sessions & stats pub device_id: Option, pub created_at: NaiveDateTime, - pub connected_at: NaiveDateTime, - pub disconnected_at: NaiveDateTime, + pub connected_at: Option, + pub disconnected_at: Option, pub mfa: bool, #[model(enum)] pub state: VpnClientSessionState, } + +impl VpnClientSession { + pub fn new( + location_id: Id, + user_id: Id, + device_id: Id, + connected_at: Option, + mfa: bool, + ) -> Self { + // determine session state + let state = if connected_at.is_some() { + VpnClientSessionState::Connected + } else { + VpnClientSessionState::New + }; + + Self { + id: NoId, + location_id, + user_id, + device_id: Some(device_id), + created_at: Utc::now().naive_utc(), + connected_at, + disconnected_at: None, + mfa, + state, + } + } +} diff --git a/crates/defguard_common/src/messages/peer_stats_update.rs b/crates/defguard_common/src/messages/peer_stats_update.rs index 4ec89b70e3..3ba219387b 100644 --- a/crates/defguard_common/src/messages/peer_stats_update.rs +++ b/crates/defguard_common/src/messages/peer_stats_update.rs @@ -6,6 +6,7 @@ use crate::db::Id; /// Represents stats read from a WireGuard interface /// sent from a gateway +#[derive(Debug)] pub struct PeerStatsUpdate { pub location_id: Id, pub device_id: Id, diff --git a/crates/defguard_session_manager/src/error.rs b/crates/defguard_session_manager/src/error.rs index c9bc5c4dd7..473baf39c5 100644 --- a/crates/defguard_session_manager/src/error.rs +++ b/crates/defguard_session_manager/src/error.rs @@ -17,6 +17,8 @@ pub enum SessionManagerError { UserDoesNotExistError(Id), #[error("Device with ID {0} does not exist")] DeviceDoesNotExistError(Id), + #[error("Location with ID {0} does not exist")] + LocationDoesNotExistError(Id), #[error("Session map initialization error: {0}")] SessionMapInitializationError(String), } diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index cf19be0d98..949a8c278d 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -1,12 +1,10 @@ -use std::collections::HashMap; - use defguard_common::messages::peer_stats_update::PeerStatsUpdate; -use sqlx::PgPool; +use sqlx::{PgConnection, PgPool}; use tokio::{ sync::mpsc::UnboundedReceiver, time::{Duration, interval}, }; -use tracing::{debug, info}; +use tracing::{debug, error, info, trace, warn}; use crate::{error::SessionManagerError, session_state::LocationSessionsMap}; @@ -23,52 +21,116 @@ pub async fn run_session_manager( info!("Starting VPN client session manager service"); let mut session_update_timer = interval(Duration::from_secs(SESSION_UPDATE_INTERVAL)); - // initialize active sessions state based on DB content - let mut active_sessions = LocationSessionsMap::initialize_from_db(&pool).await?; + // initialize session manager + let mut session_manager = SessionManager::new(pool).await?; loop { // receive next batch of peer stats messages // if no message is received within `SESSION_UPDATE_INTERVAL` trigger session status refresh anyway // to disconnect inactive sessions if necessary let mut message_buffer: Vec = Vec::with_capacity(MESSAGE_LIMIT); - let message_count = tokio::select! { + let _message_count = tokio::select! { message_count = peer_stats_rx.recv_many(&mut message_buffer, MESSAGE_LIMIT) => message_count, _ = session_update_timer.tick() => { - info!("No wireguard peer stats updates received in last {SESSION_UPDATE_INTERVAL}. Triggering session status update."); - active_sessions.update_session_status(); + warn!("No wireguard peer stats updates received in last {SESSION_UPDATE_INTERVAL}. Triggering session status update to disconnect inactive clients."); + session_manager.update_inactive_session_status().await?; + + // skip to next iteration continue; } }; - debug!("Processing batch of {message_count} peer stats updates"); + // process received messages to update active sessions + session_manager + .process_message_batch(message_buffer) + .await?; + + // update inactive/disconnected sessions + session_manager.update_inactive_session_status().await?; + } +} + +struct SessionManager { + pool: PgPool, + active_sessions: LocationSessionsMap, +} + +impl SessionManager { + async fn new(pool: PgPool) -> Result { + // initialize active sessions state based on DB content + let active_sessions = LocationSessionsMap::initialize_from_db(&pool).await?; - // create temporary maps of DB objects to avoid repeated queries - // let location_map = HashMap::new(); - // let user_map = HashMap::new(); - // let device_map = HashMap::new(); + Ok(Self { + pool, + active_sessions, + }) + } + + /// Helper function for processing all messages read from the channel in a single batch + /// + /// This should only fail if there's an issue with a DB transaction. + /// Otherwise we just log an error and move on to the next message. + async fn process_message_batch( + &mut self, + messages: Vec, + ) -> Result<(), SessionManagerError> { + debug!("Processing batch of {} peer stats updates", messages.len()); // begin DB transaction - let transaction = pool.begin().await?; - - for message in message_buffer { - // check if a session exists already for a given peer - match active_sessions.try_get_peer_session() { - Some(mut session) => { - // session exists already, update it based on received stats - session.update(message); - } - None => { - debug!( - "No active session found for device {} in location {}. Creating a new session", - message.device_id, message.location_id - ); - active_sessions.new_session(); - } + let mut transaction = self.pool.begin().await?; + + for message in messages { + if let Err(err) = self + .process_single_message(&mut *transaction, message) + .await + { + error!("Failed to process peer stats update: {err}"); } } // commit DB transaction after processing all messages transaction.commit().await?; + + debug!("Finished processing message batch."); + + Ok(()) + } + + /// Helper function for processing a single message + async fn process_single_message( + &mut self, + transaction: &mut PgConnection, + message: PeerStatsUpdate, + ) -> Result<(), SessionManagerError> { + trace!("Processing peer stats update: {message:?}"); + + // check if a session exists already for a given peer + // and create one if necessary + let mut session = match self + .active_sessions + .try_get_peer_session(message.location_id, message.device_id) + { + Some(session) => session, + None => { + debug!( + "No active session found for device {} in location {}. Creating a new session", + message.device_id, message.location_id + ); + self.active_sessions + .new_session(transaction, &message) + .await? + } + }; + + // update session stats + session.update_stats(message); + + trace!("Finished processing peer stats update"); + Ok(()) + } + + async fn update_inactive_session_status(&self) -> Result<(), SessionManagerError> { + unimplemented!() } } diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index ba8d8b7fac..30884e378a 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -7,7 +7,7 @@ use defguard_common::{ }, messages::peer_stats_update::PeerStatsUpdate, }; -use sqlx::PgPool; +use sqlx::{PgConnection, PgPool}; use tracing::{debug, error}; use crate::error::SessionManagerError; @@ -29,8 +29,9 @@ impl SessionState { username: user.username.clone(), } } - /// Updates session state based on received peer update - pub(crate) fn update(&mut self, peer_stats_update: PeerStatsUpdate) { + + /// Updates session stats based on received peer update + pub(crate) fn update_stats(&mut self, peer_stats_update: PeerStatsUpdate) { todo!() } } @@ -115,7 +116,11 @@ impl LocationSessionsMap { } /// Checks if a session for a given peer exists already - pub(crate) fn try_get_peer_session(&self) -> Option { + pub(crate) fn try_get_peer_session( + &self, + location_id: Id, + device_id: Id, + ) -> Option { todo!() } @@ -129,20 +134,66 @@ impl LocationSessionsMap { } /// Creates a new VPN client session, adds it to curent state and persists it in DB - pub(crate) fn new_session(&mut self) { + /// + /// We assume that at this point it's been checked that a session for this client does not exist yet. + pub(crate) async fn new_session( + &mut self, + transaction: &mut PgConnection, + stats_update: &PeerStatsUpdate, + ) -> Result { + // fetch related objects from DB + let location = Self::fetch_location(transaction, stats_update.location_id).await?; + let device = Self::fetch_device(transaction, stats_update.device_id).await?; + let user = Self::fetch_user(transaction, device.user_id).await?; + + debug!("Adding new VPN client session for location {location}"); + + let connected_at = todo!(); + + // create a client session object and save it to DB + let session = VpnClientSession::new( + location.id, + user.id, + device.id, + connected_at, + location.mfa_enabled(), + ) + .save(transaction) + .await?; + + let session_state = SessionState::new(session.id, &user); + todo!() + // Ok(()) } // Wrapper method which attempts to fetch User from DB and returns an error if None is found or an error occurs - async fn fetch_user(pool: &PgPool, user_id: Id) -> Result, SessionManagerError> { - User::find_by_id(pool, user_id) + async fn fetch_user<'e, E: sqlx::PgExecutor<'e>>( + executor: E, + user_id: Id, + ) -> Result, SessionManagerError> { + User::find_by_id(executor, user_id) .await? .ok_or(SessionManagerError::UserDoesNotExistError(user_id)) } + // Wrapper method which attempts to fetch Device from DB and returns an error if None is found or an error occurs - async fn fetch_device(pool: &PgPool, device_id: Id) -> Result, SessionManagerError> { - Device::find_by_id(pool, device_id) + async fn fetch_device<'e, E: sqlx::PgExecutor<'e>>( + executor: E, + device_id: Id, + ) -> Result, SessionManagerError> { + Device::find_by_id(executor, device_id) .await? .ok_or(SessionManagerError::DeviceDoesNotExistError(device_id)) } + + // Wrapper method which attempts to fetch Device from DB and returns an error if None is found or an error occurs + async fn fetch_location<'e, E: sqlx::PgExecutor<'e>>( + executor: E, + location_id: Id, + ) -> Result, SessionManagerError> { + WireguardNetwork::find_by_id(executor, location_id) + .await? + .ok_or(SessionManagerError::LocationDoesNotExistError(location_id)) + } } diff --git a/flake.lock b/flake.lock index 3de9f99f55..af6dfe1313 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1763283776, - "narHash": "sha256-Y7TDFPK4GlqrKrivOcsHG8xSGqQx3A6c+i7novT85Uk=", + "lastModified": 1765186076, + "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "50a96edd8d0db6cc8db57dab6bb6d6ee1f3dc49a", + "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1763347184, - "narHash": "sha256-6QH8hpCYJxifvyHEYg+Da0BotUn03BwLIvYo3JAxuqQ=", + "lastModified": 1765334520, + "narHash": "sha256-jTof2+ir9UPmv4lWksYO6WbaXCC0nsDExrB9KZj7Dz4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "08895cce80433978d5bfd668efa41c5e24578cbd", + "rev": "db61f666aea93b28f644861fbddd37f235cc5983", "type": "github" }, "original": { From 720f5ae12dea9e028d09567ebc184190f77aee17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 19 Dec 2025 12:30:46 +0100 Subject: [PATCH 08/52] update inputs --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index af6dfe1313..ad9949e54c 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1765186076, - "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", + "lastModified": 1765779637, + "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", + "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1765334520, - "narHash": "sha256-jTof2+ir9UPmv4lWksYO6WbaXCC0nsDExrB9KZj7Dz4=", + "lastModified": 1766112155, + "narHash": "sha256-N0KUOJSIBw2fFF2ACZhwYX2e0EGaHBVPlJh7bnxcGE4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "db61f666aea93b28f644861fbddd37f235cc5983", + "rev": "2a6db3fc1c27ae77f9caa553d7609b223cb770b5", "type": "github" }, "original": { From 37b009c37d3a067650ca8edb1fd1baef9715843b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 19 Dec 2025 12:33:03 +0100 Subject: [PATCH 09/52] post-merge fixes --- crates/defguard/src/main.rs | 2 +- crates/defguard_session_manager/src/session_state.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 098aeea1be..a760fc9c89 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -37,7 +37,7 @@ use defguard_event_logger::{message::EventLoggerMessage, run_event_logger}; use defguard_event_router::{RouterReceiverSet, run_event_router}; use defguard_mail::{Mail, run_mail_handler}; use defguard_proxy_manager::{ProxyOrchestrator, ProxyTxSet}; -// use defguard_session_manager::run_session_manager; +use defguard_session_manager::run_session_manager; use secrecy::ExposeSecret; use tokio::sync::{broadcast, mpsc::unbounded_channel}; diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index 30884e378a..ed4966a389 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -142,9 +142,9 @@ impl LocationSessionsMap { stats_update: &PeerStatsUpdate, ) -> Result { // fetch related objects from DB - let location = Self::fetch_location(transaction, stats_update.location_id).await?; - let device = Self::fetch_device(transaction, stats_update.device_id).await?; - let user = Self::fetch_user(transaction, device.user_id).await?; + let location = Self::fetch_location(&mut *transaction, stats_update.location_id).await?; + let device = Self::fetch_device(&mut *transaction, stats_update.device_id).await?; + let user = Self::fetch_user(&mut *transaction, device.user_id).await?; debug!("Adding new VPN client session for location {location}"); From 86b956799be4b08cc48cd8cb0249c6e3760b5096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 22 Dec 2025 09:28:27 +0100 Subject: [PATCH 10/52] update flake inputs --- flake.lock | 38 ++++++++++++++++++++++++++++++++------ flake.nix | 11 +++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index ad9949e54c..912c026838 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,17 @@ { "nodes": { + "defguard-ui": { + "flake": false, + "locked": { + "path": "web/src/shared/defguard-ui", + "type": "path" + }, + "original": { + "path": "web/src/shared/defguard-ui", + "type": "path" + }, + "parent": [] + }, "flake-utils": { "inputs": { "systems": "systems" @@ -20,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1765779637, - "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", + "lastModified": 1766070988, + "narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", + "rev": "c6245e83d836d0433170a16eb185cefe0572f8b8", "type": "github" }, "original": { @@ -34,10 +46,24 @@ "type": "github" } }, + "proto": { + "flake": false, + "locked": { + "path": "proto", + "type": "path" + }, + "original": { + "path": "proto", + "type": "path" + }, + "parent": [] + }, "root": { "inputs": { + "defguard-ui": "defguard-ui", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", + "proto": "proto", "rust-overlay": "rust-overlay" } }, @@ -48,11 +74,11 @@ ] }, "locked": { - "lastModified": 1766112155, - "narHash": "sha256-N0KUOJSIBw2fFF2ACZhwYX2e0EGaHBVPlJh7bnxcGE4=", + "lastModified": 1766371695, + "narHash": "sha256-W7CX9vy7H2Jj3E8NI4djHyF8iHSxKpb2c/7uNQ/vGFU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "2a6db3fc1c27ae77f9caa553d7609b223cb770b5", + "rev": "d81285ba8199b00dc31847258cae3c655b605e8c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 1ee1a20d86..abaeea210b 100644 --- a/flake.nix +++ b/flake.nix @@ -10,6 +10,17 @@ nixpkgs.follows = "nixpkgs"; }; }; + + # let git manage submodules + self.submodules = true; + proto = { + url = "path:proto"; + flake = false; + }; + defguard-ui = { + url = "path:web/src/shared/defguard-ui"; + flake = false; + }; }; outputs = { From 0966d21a5a3ea026475e34518d67171a54820f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 23 Dec 2025 13:16:10 +0100 Subject: [PATCH 11/52] handle creating new sessions and add object cache --- Cargo.lock | 1 + .../tests/integration/grpc/common/mod.rs | 11 +- crates/defguard_session_manager/Cargo.toml | 1 + crates/defguard_session_manager/src/lib.rs | 33 +- .../src/session_state.rs | 328 ++++++++++++------ 5 files changed, 260 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01efbe1f8a..6149408307 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1308,6 +1308,7 @@ dependencies = [ name = "defguard_session_manager" version = "0.0.0" dependencies = [ + "chrono", "defguard_common", "sqlx", "thiserror 2.0.17", diff --git a/crates/defguard_core/tests/integration/grpc/common/mod.rs b/crates/defguard_core/tests/integration/grpc/common/mod.rs index c48750a8a4..1995dd2b57 100644 --- a/crates/defguard_core/tests/integration/grpc/common/mod.rs +++ b/crates/defguard_core/tests/integration/grpc/common/mod.rs @@ -1,7 +1,9 @@ use std::sync::{Arc, Mutex}; use axum::http::Uri; -use defguard_common::db::models::settings::initialize_current_settings; +use defguard_common::{ + db::models::settings::initialize_current_settings, messages::peer_stats_update::PeerStatsUpdate, +}; use defguard_core::{ auth::failed_login::FailedLoginMap, db::AppEvent, @@ -37,6 +39,8 @@ pub struct TestGrpcServer { gateway_state: Arc>, client_state: Arc>, pub client_channel: Channel, + #[allow(dead_code)] + peer_stats_rx: UnboundedReceiver, } impl TestGrpcServer { @@ -49,6 +53,7 @@ impl TestGrpcServer { gateway_state: Arc>, client_state: Arc>, client_channel: Channel, + peer_stats_rx: UnboundedReceiver, ) -> Self { // spawn test gRPC server let grpc_server_task_handle = tokio::spawn(async move { @@ -66,6 +71,7 @@ impl TestGrpcServer { gateway_state, client_state, client_channel, + peer_stats_rx, } } @@ -128,6 +134,7 @@ pub(crate) async fn make_grpc_test_server(pool: &PgPool) -> TestGrpcServer { let worker_state = Arc::new(Mutex::new(WorkerState::new(app_event_tx.clone()))); let (wg_tx, _wg_rx) = broadcast::channel::(16); let (mail_tx, _mail_rx) = unbounded_channel::(); + let (peer_stats_tx, peer_stats_rx) = unbounded_channel::(); let gateway_state = Arc::new(Mutex::new(GatewayMap::new())); let client_state = Arc::new(Mutex::new(ClientMap::new())); @@ -164,6 +171,7 @@ pub(crate) async fn make_grpc_test_server(pool: &PgPool) -> TestGrpcServer { failed_logins, grpc_event_tx, Default::default(), + peer_stats_tx, ) .await .unwrap(); @@ -176,6 +184,7 @@ pub(crate) async fn make_grpc_test_server(pool: &PgPool) -> TestGrpcServer { gateway_state, client_state, client_channel, + peer_stats_rx, ) .await } diff --git a/crates/defguard_session_manager/Cargo.toml b/crates/defguard_session_manager/Cargo.toml index acd7ebfbe0..e72d7ce996 100644 --- a/crates/defguard_session_manager/Cargo.toml +++ b/crates/defguard_session_manager/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] defguard_common.workspace = true +chrono.workspace = true sqlx.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 949a8c278d..238f845044 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -1,12 +1,12 @@ use defguard_common::messages::peer_stats_update::PeerStatsUpdate; -use sqlx::{PgConnection, PgPool}; +use sqlx::{PgConnection, PgPool, types::chrono::NaiveDateTime}; use tokio::{ sync::mpsc::UnboundedReceiver, time::{Duration, interval}, }; use tracing::{debug, error, info, trace, warn}; -use crate::{error::SessionManagerError, session_state::LocationSessionsMap}; +use crate::{error::SessionManagerError, session_state::ActiveSessionsMap}; pub mod error; pub mod session_state; @@ -53,17 +53,17 @@ pub async fn run_session_manager( struct SessionManager { pool: PgPool, - active_sessions: LocationSessionsMap, + // active_sessions: LocationSessionsMap, } impl SessionManager { async fn new(pool: PgPool) -> Result { // initialize active sessions state based on DB content - let active_sessions = LocationSessionsMap::initialize_from_db(&pool).await?; + // let active_sessions = LocationSessionsMap::initialize_from_db(&pool).await?; Ok(Self { pool, - active_sessions, + // active_sessions, }) } @@ -80,9 +80,12 @@ impl SessionManager { // begin DB transaction let mut transaction = self.pool.begin().await?; + // initialize session map + let mut active_sessions = ActiveSessionsMap::new(); + for message in messages { if let Err(err) = self - .process_single_message(&mut *transaction, message) + .process_single_message(&mut *transaction, &mut active_sessions, message) .await { error!("Failed to process peer stats update: {err}"); @@ -101,30 +104,32 @@ impl SessionManager { async fn process_single_message( &mut self, transaction: &mut PgConnection, + active_sessions: &mut ActiveSessionsMap, message: PeerStatsUpdate, ) -> Result<(), SessionManagerError> { trace!("Processing peer stats update: {message:?}"); // check if a session exists already for a given peer - // and create one if necessary - let mut session = match self - .active_sessions + // and attempt to add one if necessary + let maybe_session = match active_sessions .try_get_peer_session(message.location_id, message.device_id) { - Some(session) => session, + Some(session) => Some(session), None => { debug!( "No active session found for device {} in location {}. Creating a new session", message.device_id, message.location_id ); - self.active_sessions - .new_session(transaction, &message) + active_sessions + .try_add_new_session(transaction, &message) .await? } }; - // update session stats - session.update_stats(message); + if let Some(mut session) = maybe_session { + // update session stats + session.update_stats(message)?; + }; trace!("Finished processing peer stats update"); Ok(()) diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index ed4966a389..f0eb5160a7 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use chrono::TimeDelta; use defguard_common::{ db::{ Id, @@ -7,8 +8,8 @@ use defguard_common::{ }, messages::peer_stats_update::PeerStatsUpdate, }; -use sqlx::{PgConnection, PgPool}; -use tracing::{debug, error}; +use sqlx::{PgConnection, PgPool, types::chrono::Utc}; +use tracing::{debug, error, warn}; use crate::error::SessionManagerError; @@ -31,8 +32,18 @@ impl SessionState { } /// Updates session stats based on received peer update - pub(crate) fn update_stats(&mut self, peer_stats_update: PeerStatsUpdate) { - todo!() + pub(crate) fn update_stats( + &mut self, + peer_stats_update: PeerStatsUpdate, + ) -> Result<(), SessionManagerError> { + // get previous stats + todo!(); + + // calculate transfer change + todo!(); + + // store stats update in DB + todo!(); } } @@ -40,92 +51,114 @@ impl SessionState { pub(crate) struct SessionMap(HashMap); impl SessionMap { + pub(crate) fn new() -> Self { + Self(HashMap::new()) + } + /// Helper to insert into inner map fn insert(&mut self, key: Id, session_state: SessionState) -> Option { self.0.insert(key, session_state) } } -/// Helper struct to hold session maps for all locations -pub(crate) struct LocationSessionsMap(HashMap); +/// Helper struct to hold session maps for all locations and object cache to avoid repeated DB queries +/// +/// Since we want to support HA core deployments this structure +/// is not meant to be the source of truth, but rather a cache +/// to avoid repeated DB queries when processing a single batch of messages. +/// After a batch is processed it should be discarded and a new `ActiveSessionsMap` +/// should be created for the next batch. +pub(crate) struct ActiveSessionsMap { + sessions: HashMap, + locations: HashMap>, + users: HashMap>, + devices: HashMap>, +} + +impl ActiveSessionsMap { + pub(crate) fn new() -> Self { + Self { + sessions: HashMap::new(), + locations: HashMap::new(), + users: HashMap::new(), + devices: HashMap::new(), + } + } -impl LocationSessionsMap { /// Helper to insert into inner map fn insert(&mut self, key: Id, session_map: SessionMap) -> Option { - self.0.insert(key, session_map) + self.sessions.insert(key, session_map) } } -impl LocationSessionsMap { +impl ActiveSessionsMap { /// Fetch current active sessions for all locations from DB /// and initialize session map - pub(crate) async fn initialize_from_db(pool: &PgPool) -> Result { - debug!("Initializing active sessions map from DB"); - - // initialize empty map - let mut active_sessions = LocationSessionsMap(HashMap::new()); - - // fetch all locations - let locations = WireguardNetwork::all(pool).await?; - - // get active sessions for all locations - for location in locations { - // fetch active sessions from DB - let location_sessions = location.get_active_vpn_sessions(pool).await?; - - // initialize empty session map for a given location - let mut location_session_map = SessionMap(HashMap::new()); - - // insert sessions into map - for session in location_sessions { - // we can unwrap here since active session must have a device ID - let device_id = session - .device_id - .expect("Active session must have device_id"); - - let device = Self::fetch_device(pool, device_id).await?; - - let user = Self::fetch_user(pool, session.user_id).await?; - - let session_state = SessionState::new(session.id, &user); - - if let Some(existing_session) = - location_session_map.insert(device_id, session_state) - { - error!( - "Found duplicate active session for device {device} in location {location}" - ); - return Err(SessionManagerError::MultipleActiveSessionsError { - location_name: location.name, - username: existing_session.username, - device_name: device.name, - }); - }; - } - - if let Some(_) = active_sessions.insert(location.id, location_session_map) { - let msg = format!( - "Active sessions for location {location} have already been initialized" - ); - error!("{msg}"); - return Err(SessionManagerError::SessionMapInitializationError(msg)); - }; - } - - Ok(active_sessions) - } + // pub(crate) async fn initialize_from_db(pool: &PgPool) -> Result { + // debug!("Initializing active sessions map from DB"); + + // // initialize empty map + // let mut active_sessions = LocationSessionsMap(HashMap::new()); + + // // fetch all locations + // let locations = WireguardNetwork::all(pool).await?; + + // // get active sessions for all locations + // for location in locations { + // // fetch active sessions from DB + // let location_sessions = location.get_active_vpn_sessions(pool).await?; + + // // initialize empty session map for a given location + // let mut location_session_map = SessionMap(HashMap::new()); + + // // insert sessions into map + // for session in location_sessions { + // // we can unwrap here since active session must have a device ID + // let device_id = session + // .device_id + // .expect("Active session must have device_id"); + + // let device = Self::fetch_device(pool, device_id).await?; + + // let user = Self::fetch_user(pool, session.user_id).await?; + + // let session_state = SessionState::new(session.id, &user); + + // if let Some(existing_session) = + // location_session_map.insert(device_id, session_state) + // { + // error!( + // "Found duplicate active session for device {device} in location {location}" + // ); + // return Err(SessionManagerError::MultipleActiveSessionsError { + // location_name: location.name, + // username: existing_session.username, + // device_name: device.name, + // }); + // }; + // } + + // if let Some(_) = active_sessions.insert(location.id, location_session_map) { + // let msg = format!( + // "Active sessions for location {location} have already been initialized" + // ); + // error!("{msg}"); + // return Err(SessionManagerError::SessionMapInitializationError(msg)); + // }; + // } + + // Ok(active_sessions) + // } /// Checks if a session for a given peer exists already pub(crate) fn try_get_peer_session( - &self, + &mut self, location_id: Id, device_id: Id, - ) -> Option { - todo!() - } - - pub(crate) fn get_location_sessions(&self, location_id: Id) { - todo!() + ) -> Option<&mut SessionState> { + self.sessions + .get_mut(&location_id) + .map(|session_map| session_map.0.get_mut(&device_id))? } /// Checks if any sessions need to be marked as disconnected @@ -133,67 +166,164 @@ impl LocationSessionsMap { todo!() } - /// Creates a new VPN client session, adds it to curent state and persists it in DB + /// Attempts to create a new VPN client session, add it to curent state and persists it in DB /// - /// We assume that at this point it's been checked that a session for this client does not exist yet. - pub(crate) async fn new_session( + /// We assume that at this point it's been checked that a session for this client does not exist yet, + /// but we do check if given peer can be considered active based on a given locations peer disconnect threshold. + pub(crate) async fn try_add_new_session( &mut self, transaction: &mut PgConnection, stats_update: &PeerStatsUpdate, - ) -> Result { - // fetch related objects from DB - let location = Self::fetch_location(&mut *transaction, stats_update.location_id).await?; - let device = Self::fetch_device(&mut *transaction, stats_update.device_id).await?; - let user = Self::fetch_user(&mut *transaction, device.user_id).await?; + ) -> Result, SessionManagerError> { + // fetch location + let location_id = stats_update.location_id; + let location = self.get_location(&mut *transaction, location_id).await?; + + // check if a given peer is considered active and should be added to active sessions + if Utc::now().naive_utc() - stats_update.latest_handshake + > TimeDelta::seconds(location.peer_disconnect_threshold.into()) + { + warn!( + "Received peer stats update for an inactive peer. Skipping creating a new session..." + ); + return Ok(None); + } - debug!("Adding new VPN client session for location {location}"); + // fetch other related objects from DB + let device_id = stats_update.device_id; + let device = self.get_device(&mut *transaction, device_id).await?; + let user = self.get_user(&mut *transaction, device.user_id).await?; - let connected_at = todo!(); + debug!("Adding new VPN client session for location {location}"); // create a client session object and save it to DB let session = VpnClientSession::new( location.id, user.id, device.id, - connected_at, + Some(stats_update.latest_handshake), location.mfa_enabled(), ) .save(transaction) .await?; + // add to session map let session_state = SessionState::new(session.id, &user); + let session_map = self.get_or_create_location_session_map(location_id); + let maybe_existing_session = session_map.insert(device_id, session_state); + // if a session exists already there was an error in earlier logic + assert!(maybe_existing_session.is_none()); + + Ok(Some( + session_map + .0 + .get_mut(&device_id) + .expect("Session has just been created"), + )) + } - todo!() - // Ok(()) + fn get_or_create_location_session_map(&mut self, location_id: Id) -> &mut SessionMap { + // check if location is already present in session map + if self.sessions.contains_key(&location_id) { + self.sessions + .get_mut(&location_id) + .expect("Location session map must exist") + } else { + debug!("Session map for location {location_id} not found. Initializing a new map."); + let new_session_map = SessionMap::new(); + let maybe_existing_map = self.sessions.insert(location_id, new_session_map); + // if a map exists already there was an error in earlier logic + assert!(maybe_existing_map.is_none()); + self.sessions + .get_mut(&location_id) + .expect("Location session map has just been created") + } } - // Wrapper method which attempts to fetch User from DB and returns an error if None is found or an error occurs - async fn fetch_user<'e, E: sqlx::PgExecutor<'e>>( + // Helper method which checks if User is already cached, + // then attempts to fetch User from DB and returns an error if None is found or an error occurs + async fn get_user<'e, E: sqlx::PgExecutor<'e>>( + &mut self, executor: E, user_id: Id, ) -> Result, SessionManagerError> { - User::find_by_id(executor, user_id) - .await? - .ok_or(SessionManagerError::UserDoesNotExistError(user_id)) + // first try to find user in object cache + let user = if self.users.contains_key(&user_id) { + self.users + .get(&user_id) + .expect("User must exist in object cache") + } else { + debug!("User {user_id} not found in object cache. Trying to fetch from DB."); + let user = User::find_by_id(executor, user_id) + .await? + .ok_or(SessionManagerError::LocationDoesNotExistError(user_id))?; + // update object cache + self.users.insert(user_id, user); + self.users + .get(&user_id) + .expect("User must exist in object cache") + }; + + // TODO: figure out a way to avoid multiple mutable borrows + // and return a reference instead of cloning + Ok(user.clone()) } - // Wrapper method which attempts to fetch Device from DB and returns an error if None is found or an error occurs - async fn fetch_device<'e, E: sqlx::PgExecutor<'e>>( + // Helper method which checks if Device is already cached, + // then attempts to fetch Device from DB and returns an error if None is found or an error occurs + async fn get_device<'e, E: sqlx::PgExecutor<'e>>( + &mut self, executor: E, device_id: Id, ) -> Result, SessionManagerError> { - Device::find_by_id(executor, device_id) - .await? - .ok_or(SessionManagerError::DeviceDoesNotExistError(device_id)) + // first try to find device in object cache + let device = if self.devices.contains_key(&device_id) { + self.devices + .get(&device_id) + .expect("Device must exist in object cache") + } else { + debug!("Device {device_id} not found in object cache. Trying to fetch from DB."); + let device = Device::find_by_id(executor, device_id) + .await? + .ok_or(SessionManagerError::DeviceDoesNotExistError(device_id))?; + // update object cache + self.devices.insert(device_id, device); + self.devices + .get(&device_id) + .expect("Device must exist in object cache") + }; + + // TODO: figure out a way to avoid multiple mutable borrows + // and return a reference instead of cloning + Ok(device.clone()) } - // Wrapper method which attempts to fetch Device from DB and returns an error if None is found or an error occurs - async fn fetch_location<'e, E: sqlx::PgExecutor<'e>>( + // Helper method which checks if Location is already cached, + // then attempts to fetch Location from DB and returns an error if None is found or an error occurs + async fn get_location<'e, E: sqlx::PgExecutor<'e>>( + &mut self, executor: E, location_id: Id, ) -> Result, SessionManagerError> { - WireguardNetwork::find_by_id(executor, location_id) - .await? - .ok_or(SessionManagerError::LocationDoesNotExistError(location_id)) + // first try to find location in object cache + let location = if self.locations.contains_key(&location_id) { + self.locations + .get(&location_id) + .expect("Location must exist in object cache") + } else { + debug!("Location {location_id} not found in object cache. Trying to fetch from DB."); + let location = WireguardNetwork::find_by_id(executor, location_id) + .await? + .ok_or(SessionManagerError::LocationDoesNotExistError(location_id))?; + // update object cache + self.locations.insert(location_id, location); + self.locations + .get(&location_id) + .expect("Location must exist in object cache") + }; + + // TODO: figure out a way to avoid multiple mutable borrows + // and return a reference instead of cloning + Ok(location.clone()) } } From 483c5ee0d36e2f215c61d67047d4451026f34bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 2 Jan 2026 08:59:18 +0100 Subject: [PATCH 12/52] update stats model --- .../defguard_common/src/db/models/vpn_session_stats.rs | 9 +++++++++ migrations/20251121121935_vpn_client_sessions.up.sql | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/crates/defguard_common/src/db/models/vpn_session_stats.rs b/crates/defguard_common/src/db/models/vpn_session_stats.rs index cdbd6b7c9d..fd879acce6 100644 --- a/crates/defguard_common/src/db/models/vpn_session_stats.rs +++ b/crates/defguard_common/src/db/models/vpn_session_stats.rs @@ -1,3 +1,4 @@ +use chrono::NaiveDateTime; use model_derive::Model; use crate::db::{Id, NoId}; @@ -7,6 +8,14 @@ use crate::db::{Id, NoId}; pub struct VpnSessionStats { pub id: I, pub session_id: Id, + pub collected_at: NaiveDateTime, + // handshake must have occured for a session to be considered active + pub latest_handshake: NaiveDateTime, + pub endpoint: String, + // total bytes sent to peer as read from WireGuard interface + pub total_upload: i64, + // total bytes received from peer as read from WireGuard interface + pub total_download: i64, // uplad since last stats update pub upload_diff: i64, // download since last stats update diff --git a/migrations/20251121121935_vpn_client_sessions.up.sql b/migrations/20251121121935_vpn_client_sessions.up.sql index e47bda8934..9914cdcc88 100644 --- a/migrations/20251121121935_vpn_client_sessions.up.sql +++ b/migrations/20251121121935_vpn_client_sessions.up.sql @@ -22,6 +22,11 @@ CREATE TABLE vpn_client_session ( CREATE TABLE vpn_session_stats ( id bigserial PRIMARY KEY, session_id bigint NOT NULL, + collected_at timestamp without time zone NOT NULL, + latest_handshake timestamp without time zone NOT NULL, + endpoint text NOT NULL, + total_upload bigint NOT NULL, + total_download bigint NOT NULL, upload_diff bigint NOT NULL, download_diff bigint NOT NULL, FOREIGN KEY (session_id) REFERENCES vpn_client_session(id) ON DELETE CASCADE From b32704ed7d05ded620963d7ff5ff02ff67ea2200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 2 Jan 2026 13:36:41 +0100 Subject: [PATCH 13/52] store sessions stats updates --- .../src/db/models/vpn_client_session.rs | 34 ++++- .../src/db/models/vpn_session_stats.rs | 25 ++++ crates/defguard_session_manager/src/lib.rs | 7 +- .../src/session_state.rs | 125 +++++++++++++++--- 4 files changed, 170 insertions(+), 21 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index e9ad5ba48d..f5f8016ec7 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -1,8 +1,8 @@ use chrono::{NaiveDateTime, Utc}; use model_derive::Model; -use sqlx::Type; +use sqlx::{Error as SqlxError, Type, query_as}; -use crate::db::{Id, NoId}; +use crate::db::{Id, NoId, models::vpn_session_stats::VpnSessionStats}; #[derive(Default, Type)] #[sqlx(type_name = "vpn_client_session_state", rename_all = "lowercase")] @@ -58,3 +58,33 @@ impl VpnClientSession { } } } + +impl VpnClientSession { + /// Tries to fetch the latest active session for a given location and device + /// + /// A session is considered active if it's state is `New` or `Connected` + pub async fn try_get_active_session<'e, E: sqlx::PgExecutor<'e>>( + executor: E, + location_id: Id, + device_id: Id, + ) -> Result, SqlxError> { + query_as!( + Self, + "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ + mfa, state \"state: VpnClientSessionState\" \ + FROM vpn_client_session \ + WHERE location_id = $1 AND device_id = $2", + location_id, + device_id + ) + .fetch_optional(executor) + .await + } + + pub async fn try_get_latest_stats<'e, E: sqlx::PgExecutor<'e>>( + &self, + executor: E, + ) -> Result>, SqlxError> { + unimplemented!() + } +} diff --git a/crates/defguard_common/src/db/models/vpn_session_stats.rs b/crates/defguard_common/src/db/models/vpn_session_stats.rs index fd879acce6..d2909eb4ef 100644 --- a/crates/defguard_common/src/db/models/vpn_session_stats.rs +++ b/crates/defguard_common/src/db/models/vpn_session_stats.rs @@ -21,3 +21,28 @@ pub struct VpnSessionStats { // download since last stats update pub download_diff: i64, } + +impl VpnSessionStats { + pub fn new( + session_id: Id, + collected_at: NaiveDateTime, + latest_handshake: NaiveDateTime, + endpoint: String, + total_upload: i64, + total_download: i64, + upload_diff: i64, + download_diff: i64, + ) -> Self { + Self { + id: NoId, + session_id, + collected_at, + latest_handshake, + endpoint, + total_upload, + total_download, + upload_diff, + download_diff, + } + } +} diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 238f845044..ccd2d5739f 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -112,7 +112,8 @@ impl SessionManager { // check if a session exists already for a given peer // and attempt to add one if necessary let maybe_session = match active_sessions - .try_get_peer_session(message.location_id, message.device_id) + .try_get_peer_session(transaction, message.location_id, message.device_id) + .await? { Some(session) => Some(session), None => { @@ -126,9 +127,9 @@ impl SessionManager { } }; - if let Some(mut session) = maybe_session { + if let Some(session) = maybe_session { // update session stats - session.update_stats(message)?; + session.update_stats(transaction, message).await?; }; trace!("Finished processing peer stats update"); diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index f0eb5160a7..df798e8976 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; -use chrono::TimeDelta; +use chrono::{NaiveDateTime, TimeDelta}; use defguard_common::{ db::{ Id, - models::{Device, User, WireguardNetwork, vpn_client_session::VpnClientSession}, + models::{ + Device, User, WireguardNetwork, vpn_client_session::VpnClientSession, + vpn_session_stats::VpnSessionStats, + }, }, messages::peer_stats_update::PeerStatsUpdate, }; @@ -13,12 +16,40 @@ use tracing::{debug, error, warn}; use crate::error::SessionManagerError; +struct LastStatsUpdate { + collected_at: NaiveDateTime, + latest_handshake: NaiveDateTime, + total_upload: i64, + total_download: i64, +} + +impl LastStatsUpdate { + /// Checks if the next peer stats update is valid. + /// + /// This includes following checks: + /// - new update was collected after previous + /// - transfer values are not decreased + fn validate_update(&self, new_update: &PeerStatsUpdate) -> Result<(), SessionManagerError> { + todo!() + } +} + +impl From> for LastStatsUpdate { + fn from(value: VpnSessionStats) -> Self { + Self { + collected_at: value.collected_at, + latest_handshake: value.latest_handshake, + total_upload: value.total_upload, + total_download: value.total_download, + } + } +} + /// State of a specific VPN client session pub(crate) struct SessionState { session_id: Id, user_id: Id, - username: String, - last_stats_update: Option, + last_stats_update: Option, } impl SessionState { @@ -27,23 +58,58 @@ impl SessionState { session_id, last_stats_update: None, user_id: user.id, - username: user.username.clone(), } } /// Updates session stats based on received peer update - pub(crate) fn update_stats( + pub(crate) async fn update_stats( &mut self, + transaction: &mut PgConnection, peer_stats_update: PeerStatsUpdate, ) -> Result<(), SessionManagerError> { - // get previous stats - todo!(); + // get previous stats if available and calculate transfer change + let (upload_diff, download_diff) = match &self.last_stats_update { + Some(last_stats_update) => { + // validate current update against latest value + last_stats_update.validate_update(&peer_stats_update)?; + + // calculate transfer change + ( + peer_stats_update.upload as i64 - last_stats_update.total_upload, + peer_stats_update.download as i64 - last_stats_update.total_download, + ) + } + None => (0, 0), + }; - // calculate transfer change - todo!(); + let vpn_session_stats = VpnSessionStats::new( + self.session_id, + peer_stats_update.collected_at, + peer_stats_update.latest_handshake, + peer_stats_update.endpoint.to_string(), + peer_stats_update.upload as i64, + peer_stats_update.download as i64, + upload_diff, + download_diff, + ); // store stats update in DB - todo!(); + let stats = vpn_session_stats.save(transaction).await?; + + // update latest stats + self.last_stats_update = Some(LastStatsUpdate::from(stats)); + + Ok(()) + } +} + +impl From<&VpnClientSession> for SessionState { + fn from(value: &VpnClientSession) -> Self { + Self { + session_id: value.id, + user_id: value.user_id, + last_stats_update: None, + } } } @@ -151,14 +217,41 @@ impl ActiveSessionsMap { // } /// Checks if a session for a given peer exists already - pub(crate) fn try_get_peer_session( + pub(crate) async fn try_get_peer_session( &mut self, + transaction: &mut PgConnection, location_id: Id, device_id: Id, - ) -> Option<&mut SessionState> { - self.sessions - .get_mut(&location_id) - .map(|session_map| session_map.0.get_mut(&device_id))? + ) -> Result, SessionManagerError> { + // try to get session from current map + let session_map = self.get_or_create_location_session_map(location_id); + if session_map.0.contains_key(&device_id) { + return Ok(session_map.0.get_mut(&device_id)); + } + + // session not found in current map, try to fetch from DB + let maybe_db_session = + VpnClientSession::try_get_active_session(&mut *transaction, location_id, device_id) + .await?; + + match maybe_db_session { + None => Ok(None), + Some(db_session) => { + let mut session_state = SessionState::from(&db_session); + + // try to fetch latest available stats for a given session + if let Some(latest_stats) = db_session.try_get_latest_stats(transaction).await? { + session_state.last_stats_update = Some(LastStatsUpdate::from(latest_stats)); + }; + + // put session state in map + let maybe_existing_session = session_map.insert(device_id, session_state); + // if a session exists already there was an error in earlier logic + assert!(maybe_existing_session.is_none()); + + Ok(session_map.0.get_mut(&device_id)) + } + } } /// Checks if any sessions need to be marked as disconnected From 339c3cbfcb2f455ba9429583a5463869e24473e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 2 Jan 2026 13:50:20 +0100 Subject: [PATCH 14/52] clanup unused code --- crates/defguard_session_manager/src/lib.rs | 2 +- .../src/session_state.rs | 74 +------------------ 2 files changed, 5 insertions(+), 71 deletions(-) diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index ccd2d5739f..6b24e87e05 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -1,5 +1,5 @@ use defguard_common::messages::peer_stats_update::PeerStatsUpdate; -use sqlx::{PgConnection, PgPool, types::chrono::NaiveDateTime}; +use sqlx::{PgConnection, PgPool}; use tokio::{ sync::mpsc::UnboundedReceiver, time::{Duration, interval}, diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index df798e8976..9af21884c3 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -11,8 +11,8 @@ use defguard_common::{ }, messages::peer_stats_update::PeerStatsUpdate, }; -use sqlx::{PgConnection, PgPool, types::chrono::Utc}; -use tracing::{debug, error, warn}; +use sqlx::{PgConnection, types::chrono::Utc}; +use tracing::{debug, warn}; use crate::error::SessionManagerError; @@ -150,73 +150,12 @@ impl ActiveSessionsMap { devices: HashMap::new(), } } - - /// Helper to insert into inner map - fn insert(&mut self, key: Id, session_map: SessionMap) -> Option { - self.sessions.insert(key, session_map) - } } impl ActiveSessionsMap { - /// Fetch current active sessions for all locations from DB - /// and initialize session map - // pub(crate) async fn initialize_from_db(pool: &PgPool) -> Result { - // debug!("Initializing active sessions map from DB"); - - // // initialize empty map - // let mut active_sessions = LocationSessionsMap(HashMap::new()); - - // // fetch all locations - // let locations = WireguardNetwork::all(pool).await?; - - // // get active sessions for all locations - // for location in locations { - // // fetch active sessions from DB - // let location_sessions = location.get_active_vpn_sessions(pool).await?; - - // // initialize empty session map for a given location - // let mut location_session_map = SessionMap(HashMap::new()); - - // // insert sessions into map - // for session in location_sessions { - // // we can unwrap here since active session must have a device ID - // let device_id = session - // .device_id - // .expect("Active session must have device_id"); - - // let device = Self::fetch_device(pool, device_id).await?; - - // let user = Self::fetch_user(pool, session.user_id).await?; - - // let session_state = SessionState::new(session.id, &user); - - // if let Some(existing_session) = - // location_session_map.insert(device_id, session_state) - // { - // error!( - // "Found duplicate active session for device {device} in location {location}" - // ); - // return Err(SessionManagerError::MultipleActiveSessionsError { - // location_name: location.name, - // username: existing_session.username, - // device_name: device.name, - // }); - // }; - // } - - // if let Some(_) = active_sessions.insert(location.id, location_session_map) { - // let msg = format!( - // "Active sessions for location {location} have already been initialized" - // ); - // error!("{msg}"); - // return Err(SessionManagerError::SessionMapInitializationError(msg)); - // }; - // } - - // Ok(active_sessions) - // } - /// Checks if a session for a given peer exists already + /// + /// First we check current map, then try the DB. pub(crate) async fn try_get_peer_session( &mut self, transaction: &mut PgConnection, @@ -254,11 +193,6 @@ impl ActiveSessionsMap { } } - /// Checks if any sessions need to be marked as disconnected - pub(crate) fn update_session_status(&mut self) { - todo!() - } - /// Attempts to create a new VPN client session, add it to curent state and persists it in DB /// /// We assume that at this point it's been checked that a session for this client does not exist yet, From 10badad6b59fc4c5b860000992844bc77ab1099e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 2 Jan 2026 14:19:47 +0100 Subject: [PATCH 15/52] parse endpoint --- crates/defguard_core/src/grpc/gateway/mod.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 93c1637cdc..c076024a9f 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -108,12 +108,9 @@ fn try_protos_into_stats_message( location_id: Id, device_id: Id, ) -> Option { - let endpoint = todo!(); // try to parse endpoint - // let endpoint = match proto_stats.endpoint { - // endpoint if endpoint.is_empty() => None, - // _ => Some(proto_stats.endpoint), - // }; + let endpoint = proto_stats.endpoint.parse().ok()?; + let latest_handshake = DateTime::from_timestamp(proto_stats.latest_handshake as i64, 0) .unwrap_or_default() .naive_utc(); From 5f4f6978b47f02fc05f47b92c779f1c893c36075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 2 Jan 2026 15:06:57 +0100 Subject: [PATCH 16/52] fetch latest stats for session --- .../src/db/models/vpn_client_session.rs | 12 +++++++++++- crates/defguard_core/src/grpc/mod.rs | 5 +---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index f5f8016ec7..45173ec44c 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -85,6 +85,16 @@ impl VpnClientSession { &self, executor: E, ) -> Result>, SqlxError> { - unimplemented!() + query_as!( + VpnSessionStats, + "SELECT id, session_id, collected_at, latest_handshake, endpoint, \ + total_upload, total_download, upload_diff, download_diff + FROM vpn_session_stats \ + WHERE session_id = $1 \ + ORDER BY collected_at DESC LIMIT 1", + self.id + ) + .fetch_optional(executor) + .await } } diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index d295122e9e..6cf580ff74 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -15,10 +15,7 @@ use tower::ServiceBuilder; use defguard_common::{ VERSION, auth::claims::ClaimsType, - db::{ - Id, - models::{Settings, wireguard_peer_stats::WireguardPeerStats}, - }, + db::{Id, models::Settings}, messages::peer_stats_update::PeerStatsUpdate, }; use defguard_mail::Mail; From 9d1cf471959d2f0e0fed07c7a8d5d1134d4539dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 5 Jan 2026 10:12:59 +0100 Subject: [PATCH 17/52] validate peer stats update order --- crates/defguard_session_manager/src/error.rs | 4 ++-- .../src/session_state.rs | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/defguard_session_manager/src/error.rs b/crates/defguard_session_manager/src/error.rs index 473baf39c5..95bf4f5496 100644 --- a/crates/defguard_session_manager/src/error.rs +++ b/crates/defguard_session_manager/src/error.rs @@ -19,6 +19,6 @@ pub enum SessionManagerError { DeviceDoesNotExistError(Id), #[error("Location with ID {0} does not exist")] LocationDoesNotExistError(Id), - #[error("Session map initialization error: {0}")] - SessionMapInitializationError(String), + #[error("Received out of order peer stats update")] + PeerStatsUpdateOutOfOrderError, } diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index 9af21884c3..df95545550 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -30,7 +30,21 @@ impl LastStatsUpdate { /// - new update was collected after previous /// - transfer values are not decreased fn validate_update(&self, new_update: &PeerStatsUpdate) -> Result<(), SessionManagerError> { - todo!() + if new_update.collected_at < self.collected_at { + return Err(SessionManagerError::PeerStatsUpdateOutOfOrderError); + } + + if new_update.latest_handshake < self.latest_handshake { + return Err(SessionManagerError::PeerStatsUpdateOutOfOrderError); + } + + if (new_update.upload as i64) < self.total_upload + || (new_update.download as i64) < self.total_download + { + return Err(SessionManagerError::PeerStatsUpdateOutOfOrderError); + } + + Ok(()) } } From a8e12b38bbc1a4461bb55f24345cbbe18b82b9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 5 Jan 2026 11:14:26 +0100 Subject: [PATCH 18/52] simplify object cache access --- crates/defguard_session_manager/src/lib.rs | 2 +- .../src/session_state.rs | 169 ++++++++---------- 2 files changed, 76 insertions(+), 95 deletions(-) diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 6b24e87e05..e677002fff 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -85,7 +85,7 @@ impl SessionManager { for message in messages { if let Err(err) = self - .process_single_message(&mut *transaction, &mut active_sessions, message) + .process_single_message(&mut transaction, &mut active_sessions, message) .await { error!("Failed to process peer stats update: {err}"); diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index df95545550..cd2338c319 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, hash_map::Entry}; use chrono::{NaiveDateTime, TimeDelta}; use defguard_common::{ @@ -67,11 +67,11 @@ pub(crate) struct SessionState { } impl SessionState { - fn new(session_id: Id, user: &User) -> Self { + fn new(session_id: Id, user_id: Id) -> Self { Self { session_id, last_stats_update: None, - user_id: user.id, + user_id, } } @@ -218,66 +218,60 @@ impl ActiveSessionsMap { ) -> Result, SessionManagerError> { // fetch location let location_id = stats_update.location_id; - let location = self.get_location(&mut *transaction, location_id).await?; - - // check if a given peer is considered active and should be added to active sessions - if Utc::now().naive_utc() - stats_update.latest_handshake - > TimeDelta::seconds(location.peer_disconnect_threshold.into()) - { - warn!( - "Received peer stats update for an inactive peer. Skipping creating a new session..." - ); - return Ok(None); - } + // wrap in block to avoid multiple mutable borrows + let (location_name, mfa_enabled) = { + let location = self.get_location(&mut *transaction, location_id).await?; + // check if a given peer is considered active and should be added to active sessions + if Utc::now().naive_utc() - stats_update.latest_handshake + > TimeDelta::seconds(location.peer_disconnect_threshold.into()) + { + warn!( + "Received peer stats update for an inactive peer. Skipping creating a new session..." + ); + return Ok(None); + }; + + (location.name.clone(), location.mfa_enabled()) + }; // fetch other related objects from DB let device_id = stats_update.device_id; - let device = self.get_device(&mut *transaction, device_id).await?; - let user = self.get_user(&mut *transaction, device.user_id).await?; + // wrap in block to avoid multiple mutable borrows + let user_id = { self.get_device(&mut *transaction, device_id).await?.user_id }; + let user = self.get_user(&mut *transaction, user_id).await?; - debug!("Adding new VPN client session for location {location}"); + debug!("Adding new VPN client session for location {location_name}"); // create a client session object and save it to DB let session = VpnClientSession::new( - location.id, + location_id, user.id, - device.id, + device_id, Some(stats_update.latest_handshake), - location.mfa_enabled(), + mfa_enabled, ) .save(transaction) .await?; // add to session map - let session_state = SessionState::new(session.id, &user); + let session_state = SessionState::new(session.id, user.id); let session_map = self.get_or_create_location_session_map(location_id); let maybe_existing_session = session_map.insert(device_id, session_state); // if a session exists already there was an error in earlier logic assert!(maybe_existing_session.is_none()); - Ok(Some( - session_map - .0 - .get_mut(&device_id) - .expect("Session has just been created"), - )) + Ok(session_map.0.get_mut(&device_id)) } fn get_or_create_location_session_map(&mut self, location_id: Id) -> &mut SessionMap { // check if location is already present in session map - if self.sessions.contains_key(&location_id) { - self.sessions - .get_mut(&location_id) - .expect("Location session map must exist") - } else { - debug!("Session map for location {location_id} not found. Initializing a new map."); - let new_session_map = SessionMap::new(); - let maybe_existing_map = self.sessions.insert(location_id, new_session_map); - // if a map exists already there was an error in earlier logic - assert!(maybe_existing_map.is_none()); - self.sessions - .get_mut(&location_id) - .expect("Location session map has just been created") + match self.sessions.entry(location_id) { + Entry::Occupied(occupied_entry) => occupied_entry.into_mut(), + Entry::Vacant(vacant_entry) => { + debug!("Session map for location {location_id} not found. Initializing a new map."); + let new_session_map = SessionMap::new(); + vacant_entry.insert(new_session_map) + } } } @@ -287,27 +281,22 @@ impl ActiveSessionsMap { &mut self, executor: E, user_id: Id, - ) -> Result, SessionManagerError> { + ) -> Result<&User, SessionManagerError> { // first try to find user in object cache - let user = if self.users.contains_key(&user_id) { - self.users - .get(&user_id) - .expect("User must exist in object cache") - } else { - debug!("User {user_id} not found in object cache. Trying to fetch from DB."); - let user = User::find_by_id(executor, user_id) - .await? - .ok_or(SessionManagerError::LocationDoesNotExistError(user_id))?; - // update object cache - self.users.insert(user_id, user); - self.users - .get(&user_id) - .expect("User must exist in object cache") + let user_entry = match self.users.entry(user_id) { + Entry::Occupied(occupied_entry) => occupied_entry, + Entry::Vacant(vacant_entry) => { + debug!("User {user_id} not found in object cache. Trying to fetch from DB."); + let user = User::find_by_id(executor, user_id) + .await? + .ok_or(SessionManagerError::UserDoesNotExistError(user_id))?; + // update object cache + vacant_entry.insert_entry(user) + } }; - // TODO: figure out a way to avoid multiple mutable borrows - // and return a reference instead of cloning - Ok(user.clone()) + // return reference to the map itself + Ok(user_entry.into_mut()) } // Helper method which checks if Device is already cached, @@ -316,27 +305,22 @@ impl ActiveSessionsMap { &mut self, executor: E, device_id: Id, - ) -> Result, SessionManagerError> { + ) -> Result<&Device, SessionManagerError> { // first try to find device in object cache - let device = if self.devices.contains_key(&device_id) { - self.devices - .get(&device_id) - .expect("Device must exist in object cache") - } else { - debug!("Device {device_id} not found in object cache. Trying to fetch from DB."); - let device = Device::find_by_id(executor, device_id) - .await? - .ok_or(SessionManagerError::DeviceDoesNotExistError(device_id))?; - // update object cache - self.devices.insert(device_id, device); - self.devices - .get(&device_id) - .expect("Device must exist in object cache") + let device_entry = match self.devices.entry(device_id) { + Entry::Occupied(occupied_entry) => occupied_entry, + Entry::Vacant(vacant_entry) => { + debug!("Device {device_id} not found in object cache. Trying to fetch from DB."); + let device = Device::find_by_id(executor, device_id) + .await? + .ok_or(SessionManagerError::DeviceDoesNotExistError(device_id))?; + // update object cache + vacant_entry.insert_entry(device) + } }; - // TODO: figure out a way to avoid multiple mutable borrows - // and return a reference instead of cloning - Ok(device.clone()) + // return reference to the map itself + Ok(device_entry.into_mut()) } // Helper method which checks if Location is already cached, @@ -345,26 +329,23 @@ impl ActiveSessionsMap { &mut self, executor: E, location_id: Id, - ) -> Result, SessionManagerError> { + ) -> Result<&WireguardNetwork, SessionManagerError> { // first try to find location in object cache - let location = if self.locations.contains_key(&location_id) { - self.locations - .get(&location_id) - .expect("Location must exist in object cache") - } else { - debug!("Location {location_id} not found in object cache. Trying to fetch from DB."); - let location = WireguardNetwork::find_by_id(executor, location_id) - .await? - .ok_or(SessionManagerError::LocationDoesNotExistError(location_id))?; - // update object cache - self.locations.insert(location_id, location); - self.locations - .get(&location_id) - .expect("Location must exist in object cache") + let location_entry = match self.locations.entry(location_id) { + Entry::Occupied(occupied_entry) => occupied_entry, + Entry::Vacant(vacant_entry) => { + debug!( + "Location {location_id} not found in object cache. Trying to fetch from DB." + ); + let location = WireguardNetwork::find_by_id(executor, location_id) + .await? + .ok_or(SessionManagerError::LocationDoesNotExistError(location_id))?; + // update object cache + vacant_entry.insert_entry(location) + } }; - // TODO: figure out a way to avoid multiple mutable borrows - // and return a reference instead of cloning - Ok(location.clone()) + // return reference to the map itself + Ok(location_entry.into_mut()) } } From 7233bc13d3af8a1c5b185a713f8bf8cc5af1eb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 5 Jan 2026 11:16:22 +0100 Subject: [PATCH 19/52] add todo for after merging GW changes --- crates/defguard_session_manager/src/session_state.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index cd2338c319..425e7cc89d 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -141,6 +141,7 @@ impl SessionMap { } } +// TODO(mwojcik): handle multiple gateways per location /// Helper struct to hold session maps for all locations and object cache to avoid repeated DB queries /// /// Since we want to support HA core deployments this structure From 9fda579351252adcf447833a25378fc8700f23bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 5 Jan 2026 13:00:34 +0100 Subject: [PATCH 20/52] update network stats query --- crates/defguard_common/src/db/models/wireguard.rs | 14 ++++++++------ crates/defguard_core/src/handlers/wireguard.rs | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 74006b7fab..7bfc1ff171 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -659,6 +659,9 @@ impl WireguardNetwork { } /// Retrieves total active users/devices since `from` timestamp + /// + /// A user/device is considered active if a session is currently connected + /// or it was disconnected at some point within the specified time window. async fn total_activity( &self, conn: &PgPool, @@ -667,15 +670,14 @@ impl WireguardNetwork { let activity_stats = query_as!( WireguardNetworkActivityStats, "SELECT \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", \ COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ - FROM wireguard_peer_stats s \ - JOIN device d ON d.id = s.device_id \ - LEFT JOIN \"user\" u ON u.id = d.user_id \ - WHERE latest_handshake >= $1 AND s.network = $2", - from, + FROM vpn_client_session s \ + LEFT JOIN device d ON d.id = s.device_id \ + WHERE s.location_id = $1 AND (s.state = 'connected' OR (s.state = 'disconnected' AND s.disconnected_at >= $2))", self.id, + from, ) .fetch_one(conn) .await?; diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 7ab97b4296..bea0990291 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -1460,15 +1460,15 @@ pub(crate) async fn network_stats( Path(network_id): Path, Query(query_from): Query, ) -> ApiResult { - debug!("Displaying WireGuard network stats for network {network_id}"); - let Some(network) = WireguardNetwork::find_by_id(&appstate.pool, network_id).await? else { + debug!("Displaying WireGuard network stats for location {network_id}"); + let Some(location) = WireguardNetwork::find_by_id(&appstate.pool, network_id).await? else { return Err(WebError::ObjectNotFound(format!( - "Requested network ({network_id}) not found" + "Requested location ({network_id}) not found" ))); }; let from = query_from.parse_timestamp()?.naive_utc(); let aggregation: DateTimeAggregation = get_aggregation(from)?; - let stats: WireguardNetworkStats = network + let stats: WireguardNetworkStats = location .network_stats(&appstate.pool, &from, &aggregation) .await?; debug!("Displayed WireGuard network stats for network {network_id}"); From 259aaa37e30209838883290228530cba88aa6e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 5 Jan 2026 13:03:17 +0100 Subject: [PATCH 21/52] disable test for now --- .../api/wireguard_network_stats.rs | 497 +++++++++--------- 1 file changed, 249 insertions(+), 248 deletions(-) diff --git a/crates/defguard_core/tests/integration/api/wireguard_network_stats.rs b/crates/defguard_core/tests/integration/api/wireguard_network_stats.rs index 21718da443..1dee142677 100644 --- a/crates/defguard_core/tests/integration/api/wireguard_network_stats.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_stats.rs @@ -27,264 +27,265 @@ struct StatsResponse { _network_devices: Vec, } -#[sqlx::test] -async fn test_stats(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; +// FIXME(mwojcik): rewrite for new stats implementation +// #[sqlx::test] +// async fn test_stats(_: PgPoolOptions, options: PgConnectOptions) { +// let pool = setup_pool(options).await; - let (client, client_state) = make_test_client(pool).await; - let pool = client_state.pool; +// let (client, client_state) = make_test_client(pool).await; +// let pool = client_state.pool; - let auth = Auth::new("admin", "pass123"); - let response = &client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); - // create network - let response = client - .post("/api/v1/network") - .json(&make_network()) - .send() - .await; - assert_eq!(response.status(), StatusCode::CREATED); +// let auth = Auth::new("admin", "pass123"); +// let response = &client.post("/api/v1/auth").json(&auth).send().await; +// assert_eq!(response.status(), StatusCode::OK); +// // create network +// let response = client +// .post("/api/v1/network") +// .json(&make_network()) +// .send() +// .await; +// assert_eq!(response.status(), StatusCode::CREATED); - // create devices - let device = json!({ - "name": "device-1", - "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", - }); - let response = client - .post("/api/v1/device/admin") - .json(&device) - .send() - .await; - assert_eq!(response.status(), StatusCode::CREATED); +// // create devices +// let device = json!({ +// "name": "device-1", +// "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", +// }); +// let response = client +// .post("/api/v1/device/admin") +// .json(&device) +// .send() +// .await; +// assert_eq!(response.status(), StatusCode::CREATED); - let device = json!({ - "name": "device-2", - "wireguard_pubkey": "sIhx53MsX+iLk83sssybHrD7M+5m+CmpLzWL/zo8C38=", - }); - let response = client - .post("/api/v1/device/admin") - .json(&device) - .send() - .await; - assert_eq!(response.status(), StatusCode::CREATED); +// let device = json!({ +// "name": "device-2", +// "wireguard_pubkey": "sIhx53MsX+iLk83sssybHrD7M+5m+CmpLzWL/zo8C38=", +// }); +// let response = client +// .post("/api/v1/device/admin") +// .json(&device) +// .send() +// .await; +// assert_eq!(response.status(), StatusCode::CREATED); - // get devices - let mut devices = Vec::>::new(); - let response = client.get("/api/v1/device/1").send().await; - assert_eq!(response.status(), StatusCode::OK); - devices.push(response.json().await); +// // get devices +// let mut devices = Vec::>::new(); +// let response = client.get("/api/v1/device/1").send().await; +// assert_eq!(response.status(), StatusCode::OK); +// devices.push(response.json().await); - let response = client.get("/api/v1/device/2").send().await; - assert_eq!(response.status(), StatusCode::OK); - devices.push(response.json().await); +// let response = client.get("/api/v1/device/2").send().await; +// assert_eq!(response.status(), StatusCode::OK); +// devices.push(response.json().await); - // empty stats - let now = Utc::now().naive_utc(); - let hour_ago = now - Duration::hours(1); - let response = client - .get(format!( - "/api/v1/network/1/stats/users?from={}", - hour_ago.format(DATE_FORMAT), - )) - .send() - .await; - assert_eq!(response.status(), StatusCode::OK); - let stats = response.json::().await; - let stats = stats.user_devices; - assert!(stats.is_empty()); +// // empty stats +// let now = Utc::now().naive_utc(); +// let hour_ago = now - Duration::hours(1); +// let response = client +// .get(format!( +// "/api/v1/network/1/stats/users?from={}", +// hour_ago.format(DATE_FORMAT), +// )) +// .send() +// .await; +// assert_eq!(response.status(), StatusCode::OK); +// let stats = response.json::().await; +// let stats = stats.user_devices; +// assert!(stats.is_empty()); - // insert stats - let samples = 60 * 11; // 11 hours of samples - for i in 0..samples { - for (d, device) in devices.iter().enumerate().take(2) { - WireguardPeerStats { - id: NoId, - device_id: device.id, - collected_at: now - Duration::minutes(i), - network: 1, - endpoint: Some("11.22.33.44".into()), - upload: (samples - i) * 10 * (d as i64 + 1), - download: (samples - i) * 20 * (d as i64 + 1), - latest_handshake: now - Duration::minutes(i * 10), - allowed_ips: Some("10.1.1.0/24".into()), - } - .save(&pool) - .await - .unwrap(); - } - } +// // insert stats +// let samples = 60 * 11; // 11 hours of samples +// for i in 0..samples { +// for (d, device) in devices.iter().enumerate().take(2) { +// WireguardPeerStats { +// id: NoId, +// device_id: device.id, +// collected_at: now - Duration::minutes(i), +// network: 1, +// endpoint: Some("11.22.33.44".into()), +// upload: (samples - i) * 10 * (d as i64 + 1), +// download: (samples - i) * 20 * (d as i64 + 1), +// latest_handshake: now - Duration::minutes(i * 10), +// allowed_ips: Some("10.1.1.0/24".into()), +// } +// .save(&pool) +// .await +// .unwrap(); +// } +// } - // minute aggregation - let response = client - .get(format!( - "/api/v1/network/1/stats/users?from={}", - hour_ago.format(DATE_FORMAT), - )) - .send() - .await; - assert_eq!(response.status(), StatusCode::OK); - let stats = response.json::().await; - let stats = stats.user_devices; - assert_eq!(stats.len(), 1); - assert_eq!(stats[0].devices.len(), 2); - assert_eq!( - stats[0].devices[0].connected_at.unwrap(), - now.trunc_subsecs(6) - ); - assert_eq!( - stats[0].devices[1].connected_at.unwrap(), - now.trunc_subsecs(6) - ); - assert_eq!(stats[0].devices[0].stats.len(), 61); - assert_eq!(stats[0].devices[1].stats.len(), 61); - let now_trunc = NaiveDate::from_ymd_opt(now.year(), now.month(), now.day()) - .unwrap_or_default() - .and_hms_opt(now.hour(), now.minute(), 0) - .unwrap_or_default(); - assert_eq!( - stats[0].devices[0].stats.last().unwrap().clone(), - WireguardDeviceTransferRow { - device_id: 1, - collected_at: now_trunc, - upload: 10, - download: 20, - } - ); - assert_eq!( - stats[0].devices[1].stats.last().unwrap().clone(), - WireguardDeviceTransferRow { - device_id: 2, - collected_at: now_trunc, - upload: 10 * 2, - download: 20 * 2, - } - ); - assert_eq!( - stats[0].devices[0] - .stats - .iter() - .map(|s| s.upload) - .sum::(), - 10 * 61 - ); - assert_eq!( - stats[0].devices[0] - .stats - .iter() - .map(|s| s.download) - .sum::(), - 20 * 61 - ); - assert_eq!( - stats[0].devices[1] - .stats - .iter() - .map(|s| s.upload) - .sum::(), - 10 * 2 * 61 - ); - assert_eq!( - stats[0].devices[1] - .stats - .iter() - .map(|s| s.download) - .sum::(), - 20 * 2 * 61 - ); +// // minute aggregation +// let response = client +// .get(format!( +// "/api/v1/network/1/stats/users?from={}", +// hour_ago.format(DATE_FORMAT), +// )) +// .send() +// .await; +// assert_eq!(response.status(), StatusCode::OK); +// let stats = response.json::().await; +// let stats = stats.user_devices; +// assert_eq!(stats.len(), 1); +// assert_eq!(stats[0].devices.len(), 2); +// assert_eq!( +// stats[0].devices[0].connected_at.unwrap(), +// now.trunc_subsecs(6) +// ); +// assert_eq!( +// stats[0].devices[1].connected_at.unwrap(), +// now.trunc_subsecs(6) +// ); +// assert_eq!(stats[0].devices[0].stats.len(), 61); +// assert_eq!(stats[0].devices[1].stats.len(), 61); +// let now_trunc = NaiveDate::from_ymd_opt(now.year(), now.month(), now.day()) +// .unwrap_or_default() +// .and_hms_opt(now.hour(), now.minute(), 0) +// .unwrap_or_default(); +// assert_eq!( +// stats[0].devices[0].stats.last().unwrap().clone(), +// WireguardDeviceTransferRow { +// device_id: 1, +// collected_at: now_trunc, +// upload: 10, +// download: 20, +// } +// ); +// assert_eq!( +// stats[0].devices[1].stats.last().unwrap().clone(), +// WireguardDeviceTransferRow { +// device_id: 2, +// collected_at: now_trunc, +// upload: 10 * 2, +// download: 20 * 2, +// } +// ); +// assert_eq!( +// stats[0].devices[0] +// .stats +// .iter() +// .map(|s| s.upload) +// .sum::(), +// 10 * 61 +// ); +// assert_eq!( +// stats[0].devices[0] +// .stats +// .iter() +// .map(|s| s.download) +// .sum::(), +// 20 * 61 +// ); +// assert_eq!( +// stats[0].devices[1] +// .stats +// .iter() +// .map(|s| s.upload) +// .sum::(), +// 10 * 2 * 61 +// ); +// assert_eq!( +// stats[0].devices[1] +// .stats +// .iter() +// .map(|s| s.download) +// .sum::(), +// 20 * 2 * 61 +// ); - assert!(stats[0].devices[0].stats[0].upload > 0); - assert!(stats[0].devices[1].stats[0].upload > 0); - assert!(stats[0].devices[0].stats[0].download > 0); - assert!(stats[0].devices[1].stats[0].download > 0); - assert_eq!(stats[0].devices[0].stats.last().unwrap().upload, 10); - assert_eq!(stats[0].devices[1].stats.last().unwrap().upload, 20); - assert_eq!(stats[0].devices[0].stats.last().unwrap().download, 20); - assert_eq!(stats[0].devices[1].stats.last().unwrap().download, 40); - assert_eq!( - stats[0].devices[0] - .stats - .iter() - .filter(|s| s.upload != 10 || s.download != 20) - .count(), - 0 - ); - assert_eq!( - stats[0].devices[1] - .stats - .iter() - .filter(|s| s.upload != 20 || s.download != 40) - .count(), - 0 - ); +// assert!(stats[0].devices[0].stats[0].upload > 0); +// assert!(stats[0].devices[1].stats[0].upload > 0); +// assert!(stats[0].devices[0].stats[0].download > 0); +// assert!(stats[0].devices[1].stats[0].download > 0); +// assert_eq!(stats[0].devices[0].stats.last().unwrap().upload, 10); +// assert_eq!(stats[0].devices[1].stats.last().unwrap().upload, 20); +// assert_eq!(stats[0].devices[0].stats.last().unwrap().download, 20); +// assert_eq!(stats[0].devices[1].stats.last().unwrap().download, 40); +// assert_eq!( +// stats[0].devices[0] +// .stats +// .iter() +// .filter(|s| s.upload != 10 || s.download != 20) +// .count(), +// 0 +// ); +// assert_eq!( +// stats[0].devices[1] +// .stats +// .iter() +// .filter(|s| s.upload != 20 || s.download != 40) +// .count(), +// 0 +// ); - // hourly aggregation - let ten_hours_ago = now - Duration::hours(10); - let ten_hours_samples = 10 * 60 + 1; - let response = client - .get(format!( - "/api/v1/network/1/stats/users?from={}", - ten_hours_ago.format(DATE_FORMAT), - )) - .send() - .await; - assert_eq!(response.status(), StatusCode::OK); - let stats = response.json::().await; - let stats = stats.user_devices; - assert_eq!(stats.len(), 1); - assert_eq!(stats[0].devices.len(), 2); - assert_eq!( - stats[0].devices[0].connected_at.unwrap(), - now.trunc_subsecs(6) - ); - assert_eq!( - stats[0].devices[1].connected_at.unwrap(), - now.trunc_subsecs(6) - ); - assert_eq!(stats[0].devices[0].stats.len(), 11); - assert_eq!(stats[0].devices[1].stats.len(), 11); - assert!(stats[0].devices[0].stats[0].upload > 0); - assert!(stats[0].devices[1].stats[0].upload > 0); - assert!(stats[0].devices[0].stats[0].download > 0); - assert!(stats[0].devices[1].stats[0].download > 0); - assert_eq!(stats[0].devices[0].stats[5].upload, 10 * 60); - assert_eq!(stats[0].devices[1].stats[5].upload, 20 * 60); - assert_eq!(stats[0].devices[0].stats[5].download, 20 * 60); - assert_eq!(stats[0].devices[1].stats[5].download, 40 * 60); +// // hourly aggregation +// let ten_hours_ago = now - Duration::hours(10); +// let ten_hours_samples = 10 * 60 + 1; +// let response = client +// .get(format!( +// "/api/v1/network/1/stats/users?from={}", +// ten_hours_ago.format(DATE_FORMAT), +// )) +// .send() +// .await; +// assert_eq!(response.status(), StatusCode::OK); +// let stats = response.json::().await; +// let stats = stats.user_devices; +// assert_eq!(stats.len(), 1); +// assert_eq!(stats[0].devices.len(), 2); +// assert_eq!( +// stats[0].devices[0].connected_at.unwrap(), +// now.trunc_subsecs(6) +// ); +// assert_eq!( +// stats[0].devices[1].connected_at.unwrap(), +// now.trunc_subsecs(6) +// ); +// assert_eq!(stats[0].devices[0].stats.len(), 11); +// assert_eq!(stats[0].devices[1].stats.len(), 11); +// assert!(stats[0].devices[0].stats[0].upload > 0); +// assert!(stats[0].devices[1].stats[0].upload > 0); +// assert!(stats[0].devices[0].stats[0].download > 0); +// assert!(stats[0].devices[1].stats[0].download > 0); +// assert_eq!(stats[0].devices[0].stats[5].upload, 10 * 60); +// assert_eq!(stats[0].devices[1].stats[5].upload, 20 * 60); +// assert_eq!(stats[0].devices[0].stats[5].download, 20 * 60); +// assert_eq!(stats[0].devices[1].stats[5].download, 40 * 60); - // network stats - let response = client - .get(format!( - "/api/v1/network/1/stats?from={}", - ten_hours_ago.format(DATE_FORMAT), - )) - .send() - .await; - assert_eq!(response.status(), StatusCode::OK); - let stats: WireguardNetworkStats = response.json().await; - assert_eq!(stats.active_users, 1); - assert_eq!(stats.active_user_devices, 2); - assert_eq!(stats.upload, ten_hours_samples * (10 + 20)); - assert_eq!(stats.download, ten_hours_samples * (20 + 40)); - assert_eq!(stats.transfer_series.len(), 11); - assert!(stats.transfer_series[0].download.is_some()); - assert!(stats.transfer_series[0].upload.is_some()); - assert_eq!(stats.transfer_series[5].upload, Some((10 + 20) * 60)); +// // network stats +// let response = client +// .get(format!( +// "/api/v1/network/1/stats?from={}", +// ten_hours_ago.format(DATE_FORMAT), +// )) +// .send() +// .await; +// assert_eq!(response.status(), StatusCode::OK); +// let stats: WireguardNetworkStats = response.json().await; +// assert_eq!(stats.active_users, 1); +// assert_eq!(stats.active_user_devices, 2); +// assert_eq!(stats.upload, ten_hours_samples * (10 + 20)); +// assert_eq!(stats.download, ten_hours_samples * (20 + 40)); +// assert_eq!(stats.transfer_series.len(), 11); +// assert!(stats.transfer_series[0].download.is_some()); +// assert!(stats.transfer_series[0].upload.is_some()); +// assert_eq!(stats.transfer_series[5].upload, Some((10 + 20) * 60)); - assert_eq!(stats.transfer_series[5].download, Some((20 + 40) * 60)); - assert_eq!( - stats.upload, - stats - .transfer_series - .iter() - .map(|v| v.upload.unwrap()) - .sum::() - ); - assert_eq!( - stats.download, - stats - .transfer_series - .iter() - .map(|v| v.download.unwrap()) - .sum::() - ); -} +// assert_eq!(stats.transfer_series[5].download, Some((20 + 40) * 60)); +// assert_eq!( +// stats.upload, +// stats +// .transfer_series +// .iter() +// .map(|v| v.upload.unwrap()) +// .sum::() +// ); +// assert_eq!( +// stats.download, +// stats +// .transfer_series +// .iter() +// .map(|v| v.download.unwrap()) +// .sum::() +// ); +// } From 7c9bb13890be0ab2df14d49755061e47b96cd36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 5 Jan 2026 13:13:37 +0100 Subject: [PATCH 22/52] rewrite remaining network stats queries --- .../src/db/models/wireguard.rs | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 7bfc1ff171..d790c8da4d 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -670,12 +670,12 @@ impl WireguardNetwork { let activity_stats = query_as!( WireguardNetworkActivityStats, "SELECT \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ - FROM vpn_client_session s \ - LEFT JOIN device d ON d.id = s.device_id \ - WHERE s.location_id = $1 AND (s.state = 'connected' OR (s.state = 'disconnected' AND s.disconnected_at >= $2))", + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ + FROM vpn_client_session s \ + LEFT JOIN device d ON d.id = s.device_id \ + WHERE s.location_id = $1 AND (s.state = 'connected' OR (s.state = 'disconnected' AND s.disconnected_at >= $2))", self.id, from, ) @@ -685,24 +685,21 @@ impl WireguardNetwork { Ok(activity_stats) } - /// Retrieves currently connected users + /// Retrieves currently connected sessions stats async fn current_activity( &self, conn: &PgPool, ) -> Result { - let from = (Utc::now() - WIREGUARD_MAX_HANDSHAKE).naive_utc(); let activity_stats = query_as!( WireguardNetworkActivityStats, "SELECT \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ - FROM wireguard_peer_stats s \ - JOIN device d ON d.id = s.device_id \ - LEFT JOIN \"user\" u ON u.id = d.user_id \ - WHERE latest_handshake >= $1 AND s.network = $2", - from, - self.id + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ + FROM vpn_client_session s \ + LEFT JOIN device d ON d.id = s.device_id \ + WHERE s.location_id = $1 AND s.state = 'connected'", + self.id, ) .fetch_one(conn) .await?; @@ -722,9 +719,10 @@ impl WireguardNetwork { WireguardStatsRow, "SELECT \ date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", \ - cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download \ - FROM wireguard_peer_stats_view \ - WHERE collected_at >= $2 AND network = $3 \ + cast(sum(upload_diff) AS bigint) upload, cast(sum(download_diff) AS bigint) download \ + FROM vpn_session_stats \ + JOIN vpn_client_session s ON session_id = s.id \ + WHERE collected_at >= $2 AND s.location_id = $3 \ GROUP BY 1 \ ORDER BY 1 \ LIMIT $4", From a33ed2b216e6164fb0a7c702471f49e52334b051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 5 Jan 2026 14:13:29 +0100 Subject: [PATCH 23/52] refactor all locations overview queries --- .../src/db/models/wireguard.rs | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index d790c8da4d..bb80ff2121 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -664,10 +664,10 @@ impl WireguardNetwork { /// or it was disconnected at some point within the specified time window. async fn total_activity( &self, - conn: &PgPool, + pool: &PgPool, from: &NaiveDateTime, ) -> Result { - let activity_stats = query_as!( + let total_activity = query_as!( WireguardNetworkActivityStats, "SELECT \ COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", \ @@ -679,18 +679,18 @@ impl WireguardNetwork { self.id, from, ) - .fetch_one(conn) + .fetch_one(pool) .await?; - Ok(activity_stats) + Ok(total_activity) } /// Retrieves currently connected sessions stats async fn current_activity( &self, - conn: &PgPool, + pool: &PgPool, ) -> Result { - let activity_stats = query_as!( + let current_activity = query_as!( WireguardNetworkActivityStats, "SELECT \ COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", \ @@ -701,17 +701,17 @@ impl WireguardNetwork { WHERE s.location_id = $1 AND s.state = 'connected'", self.id, ) - .fetch_one(conn) + .fetch_one(pool) .await?; - Ok(activity_stats) + Ok(current_activity) } /// Retrieves network upload & download time series since `from` timestamp /// using `aggregation` (hour/minute) aggregation level async fn transfer_series( &self, - conn: &PgPool, + pool: &PgPool, from: &NaiveDateTime, aggregation: &DateTimeAggregation, ) -> Result, SqlxError> { @@ -731,7 +731,7 @@ impl WireguardNetwork { self.id, PEER_STATS_LIMIT, ) - .fetch_all(conn) + .fetch_all(pool) .await?; Ok(stats) @@ -740,13 +740,13 @@ impl WireguardNetwork { /// Retrieves network stats pub async fn network_stats( &self, - conn: &PgPool, + pool: &PgPool, from: &NaiveDateTime, aggregation: &DateTimeAggregation, ) -> Result { - let total_activity = self.total_activity(conn, from).await?; - let current_activity = self.current_activity(conn).await?; - let transfer_series = self.transfer_series(conn, from, aggregation).await?; + let total_activity = self.total_activity(pool, from).await?; + let current_activity = self.current_activity(pool).await?; + let transfer_series = self.transfer_series(pool, from, aggregation).await?; Ok(WireguardNetworkStats { active_users: total_activity.active_users, active_network_devices: total_activity.active_network_devices, @@ -1137,45 +1137,47 @@ pub struct WireguardNetworkStats { } pub async fn networks_stats( - conn: &PgPool, + pool: &PgPool, from: &NaiveDateTime, aggregation: &DateTimeAggregation, ) -> Result { + // get all active users/devices within specified time window let total_activity = query_as!( WireguardNetworkActivityStats, "SELECT \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", \ COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ - FROM wireguard_peer_stats s \ - JOIN device d ON d.id = s.device_id \ - LEFT JOIN \"user\" u ON u.id = d.user_id \ - WHERE latest_handshake >= $1", + FROM vpn_client_session s \ + LEFT JOIN device d ON d.id = s.device_id \ + WHERE s.state = 'connected' OR (s.state = 'disconnected' AND s.disconnected_at >= $1)", from ) - .fetch_one(conn) + .fetch_one(pool) .await?; - let current_activity_from = (Utc::now() - WIREGUARD_MAX_HANDSHAKE).naive_utc(); + + // get all currently active users/devices let current_activity = query_as!( WireguardNetworkActivityStats, "SELECT \ - COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", \ + COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", \ COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", \ COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" \ - FROM wireguard_peer_stats s \ - JOIN device d ON d.id = s.device_id \ - LEFT JOIN \"user\" u ON u.id = d.user_id \ - WHERE latest_handshake >= $1", - current_activity_from + FROM vpn_client_session s \ + LEFT JOIN device d ON d.id = s.device_id \ + WHERE s.state = 'connected'", ) - .fetch_one(conn) + .fetch_one(pool) .await?; + + // get transfer series for specified time window let transfer_series = query_as!( WireguardStatsRow, - "SELECT \ + "SELECT \ date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", \ - cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download \ - FROM wireguard_peer_stats_view \ + cast(sum(upload_diff) AS bigint) upload, cast(sum(download_diff) AS bigint) download \ + FROM vpn_session_stats \ + JOIN vpn_client_session s ON session_id = s.id \ WHERE collected_at >= $2 \ GROUP BY 1 \ ORDER BY 1 \ @@ -1184,7 +1186,7 @@ pub async fn networks_stats( from, PEER_STATS_LIMIT, ) - .fetch_all(conn) + .fetch_all(pool) .await?; Ok(WireguardNetworkStats { current_active_users: current_activity.active_users, From c606874cc02e57f28007271f276a5bf69c864a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 8 Jan 2026 14:57:17 +0100 Subject: [PATCH 24/52] rewrite device stats queries --- .../src/db/models/wireguard.rs | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index bb80ff2121..6c1fa5dc54 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -547,33 +547,26 @@ impl WireguardNetwork { if devices.is_empty() { return Ok(Vec::new()); } - // query_as! macro doesn't work with `... WHERE ... IN (...) ` - // so we'll have to use format! macro - // https://github.com/launchbadge/sqlx/issues/875 - // https://github.com/launchbadge/sqlx/issues/656 - let device_ids = devices - .iter() - .map(|d| d.id.to_string()) - .collect::>() - .join(","); - let query = format!( - "SELECT device_id, device.name, device.user_id, \ - date_trunc($1, collected_at) collected_at, \ - CAST(sum(download) AS bigint) download, \ - CAST(sum(upload) AS bigint) upload \ - FROM wireguard_peer_stats_view wpsv \ - JOIN device ON wpsv.device_id = device.id \ - WHERE device_id IN ({device_ids}) \ - AND collected_at >= $2 \ - AND network = $3 \ - GROUP BY 1, 2, 3, 4 ORDER BY 1, 4" - ); - let stats: Vec = query_as(&query) - .bind(aggregation.fstring()) - .bind(from) - .bind(self.id) - .fetch_all(conn) - .await?; + + let device_ids = devices.iter().map(|d| d.id).collect::>(); + + let stats = query_as!( + WireguardDeviceTransferRow, + "SELECT s.device_id \"device_id!\", date_trunc($1, collected_at) \"collected_at!: NaiveDateTime\", \ + CAST(sum(download_diff) AS bigint) \"download!\", CAST(sum(upload_diff) AS bigint) \"upload!\" \ + FROM vpn_session_stats \ + INNER JOIN vpn_client_session s ON session_id = s.id \ + WHERE s.device_id = ANY($2) AND collected_at >= $3 AND s.location_id = $4 \ + GROUP BY device_id, collected_at \ + ORDER BY device_id, collected_at", + aggregation.fstring(), + &device_ids, + from, + self.id, + ) + .fetch_all(conn) + .await?; + let mut result = Vec::new(); for device in devices { let latest_stats = WireguardPeerStats::fetch_latest(conn, device.id, self.id).await?; @@ -609,21 +602,20 @@ impl WireguardNetwork { aggregation: &DateTimeAggregation, device_type: DeviceType, ) -> Result, SqlxError> { - let oldest_handshake = (Utc::now() - WIREGUARD_MAX_HANDSHAKE).naive_utc(); - // Retrieve connected devices from database + // Retrieve currently connected devices from database let devices = query_as!( Device, "SELECT DISTINCT ON (d.id) d.id, d.name, d.wireguard_pubkey, d.user_id, d.created, \ d.description, d.device_type \"device_type: DeviceType\", d.configured \ - FROM device d JOIN wireguard_peer_stats s ON d.id = s.device_id \ - WHERE s.latest_handshake >= $1 AND s.network = $2 \ - AND d.device_type = $3", - oldest_handshake, + FROM device d JOIN vpn_client_session s ON d.id = s.device_id \ + WHERE s.state = 'connected' AND s.location_id = $1 \ + AND d.device_type = $2", self.id, &device_type as &DeviceType, ) .fetch_all(conn) .await?; + // Retrieve data series for all active devices and assign them to users self.device_stats(conn, &devices, from, aggregation).await } @@ -636,6 +628,7 @@ impl WireguardNetwork { aggregation: &DateTimeAggregation, ) -> Result, SqlxError> { let mut user_map: HashMap> = HashMap::new(); + // Retrieve data series for all active devices and assign them to users let device_stats = self .distinct_device_stats(conn, from, aggregation, DeviceType::User) @@ -643,6 +636,7 @@ impl WireguardNetwork { for stats in device_stats { user_map.entry(stats.user_id).or_default().push(stats); } + // Reshape final result let mut stats = Vec::new(); for u in user_map { From 4345c8d5ad9c8d229199591904d27e03d6a4588d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 8 Jan 2026 19:36:16 +0100 Subject: [PATCH 25/52] finish refactoring user stats --- .../src/db/models/vpn_session_stats.rs | 42 +++++++++++++++++++ .../src/db/models/wireguard.rs | 25 +++++++---- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_session_stats.rs b/crates/defguard_common/src/db/models/vpn_session_stats.rs index d2909eb4ef..302a4e301d 100644 --- a/crates/defguard_common/src/db/models/vpn_session_stats.rs +++ b/crates/defguard_common/src/db/models/vpn_session_stats.rs @@ -1,5 +1,6 @@ use chrono::NaiveDateTime; use model_derive::Model; +use sqlx::{PgExecutor, query_as}; use crate::db::{Id, NoId}; @@ -46,3 +47,44 @@ impl VpnSessionStats { } } } + +impl VpnSessionStats { + /// Returns latest available stats for a given device in a given location if available + pub async fn fetch_latest_for_device<'e, E: PgExecutor<'e>>( + executor: E, + device_id: Id, + location_id: Id, + ) -> Result, sqlx::Error> { + let maybe_stats = query_as!( + Self, + "SELECT st.id, session_id, collected_at, latest_handshake, endpoint, \ + total_upload, total_download, upload_diff, download_diff \ + FROM vpn_session_stats st \ + JOIN vpn_client_session se ON session_id = se.id \ + WHERE device_id = $1 AND location_id = $2 \ + ORDER BY collected_at DESC LIMIT 1", + device_id, + location_id + ) + .fetch_optional(executor) + .await?; + + Ok(maybe_stats) + } + + /// Remove port part from `endpoint`. + /// IPv4: a.b.c.d:p -> a.b.c.d + /// IPv6: [x::y:z]:p -> x::y:z + pub fn endpoint_without_port(&self) -> Option { + // Remove port part + let mut addr = self.endpoint.rsplit_once(':')?.0; + + // Strip square brackets from IPv6 addrs + if addr.starts_with('[') && addr.ends_with(']') { + let end = addr.len() - 1; + addr = &addr[1..end]; + } + + Some(addr.to_owned()) + } +} diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 6c1fa5dc54..4915935052 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -13,7 +13,7 @@ use crate::{ ModelError, group::{Group, Permission}, vpn_client_session::{VpnClientSession, VpnClientSessionState}, - wireguard_peer_stats::WireguardPeerStats, + vpn_session_stats::VpnSessionStats, }, }, types::user_info::UserInfo, @@ -569,20 +569,31 @@ impl WireguardNetwork { let mut result = Vec::new(); for device in devices { - let latest_stats = WireguardPeerStats::fetch_latest(conn, device.id, self.id).await?; - let wireguard_ips = if let Some(stats) = &latest_stats { - stats.trim_allowed_ips() + // get public IP from latest session stats + let maybe_latest_stats = + VpnSessionStats::fetch_latest_for_device(conn, device.id, self.id).await?; + let public_ip = maybe_latest_stats + .as_ref() + .and_then(VpnSessionStats::endpoint_without_port); + + let wireguard_ips = if let Some(device_config) = + WireguardNetworkDevice::find(conn, self.id, self.id).await? + { + device_config + .wireguard_ips + .iter() + .map(|ip| ip.to_string()) + .collect() } else { Vec::new() }; + result.push(WireguardDeviceStatsRow { id: device.id, user_id: device.user_id, name: device.name.clone(), wireguard_ips, - public_ip: latest_stats - .as_ref() - .and_then(WireguardPeerStats::endpoint_without_port), + public_ip, connected_at: self.connected_at(conn, device.id).await?, // Filter stats for this device stats: stats From 584d96d649ec154aeff7d4facfd20ac5542e6fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 8 Jan 2026 20:16:03 +0100 Subject: [PATCH 26/52] get last device connection --- crates/defguard_common/src/db/models/device.rs | 18 +++++++++++++++++- .../defguard_common/src/db/models/wireguard.rs | 17 +++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index abd3b17d8b..5881f3ba7b 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -26,7 +26,7 @@ use rand::{ use serde::{Deserialize, Serialize}; use sqlx::{ Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, Type, - postgres::types::PgInterval, query, query_as, + postgres::types::PgInterval, query, query_as, query_scalar, }; use thiserror::Error; use tracing::{debug, error, info}; @@ -1002,6 +1002,22 @@ impl Device { self.user_id ).fetch_one(executor).await } + + pub async fn last_connected_at<'e, E: PgExecutor<'e>>( + &self, + executor: E, + location_id: Id, + ) -> Result, SqlxError> { + query_scalar!( + "SELECT connected_at FROM vpn_client_session \ + WHERE location_id = $1 AND device_id = $2 AND connected_at IS NOT NULL \ + ORDER BY connected_at DESC LIMIT 1", + location_id, + self.id + ) + .fetch_optional(executor) + .await + } } #[cfg(test)] diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 4915935052..cb560afa0c 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -567,6 +567,15 @@ impl WireguardNetwork { .fetch_all(conn) .await?; + // split into separate stats for each device + let mut device_stats: HashMap> = + stats.into_iter().fold(HashMap::new(), |mut acc, item| { + acc.entry(item.device_id) + .or_insert_with(Vec::new) + .push(item); + acc + }); + let mut result = Vec::new(); for device in devices { // get public IP from latest session stats @@ -594,13 +603,9 @@ impl WireguardNetwork { name: device.name.clone(), wireguard_ips, public_ip, - connected_at: self.connected_at(conn, device.id).await?, + connected_at: device.last_connected_at(conn, self.id).await?, // Filter stats for this device - stats: stats - .iter() - .filter(|s| s.device_id == device.id) - .cloned() - .collect(), + stats: device_stats.remove(&device.id).unwrap_or_default(), }); } Ok(result) From 78fa3d643fa6b255d6c044d8c66980f788e53cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 9 Jan 2026 14:19:00 +0100 Subject: [PATCH 27/52] post-merge fixes --- Cargo.lock | 1 + crates/defguard/Cargo.toml | 1 + crates/defguard/src/main.rs | 2 +- .../src/db/models/wireguard.rs | 15 ++++-- .../defguard_core/src/grpc/gateway/handler.rs | 3 +- crates/defguard_core/src/grpc/gateway/mod.rs | 46 ++++++++++--------- crates/defguard_core/src/grpc/mod.rs | 2 - proto | 2 +- 8 files changed, 40 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a68bfacb7..57b53620f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1102,6 +1102,7 @@ dependencies = [ "defguard_event_router", "defguard_mail", "defguard_proxy_manager", + "defguard_session_manager", "defguard_version", "dotenvy", "secrecy", diff --git a/crates/defguard/Cargo.toml b/crates/defguard/Cargo.toml index c6125e672c..edc03cd2eb 100644 --- a/crates/defguard/Cargo.toml +++ b/crates/defguard/Cargo.toml @@ -15,6 +15,7 @@ defguard_event_router = { workspace = true } defguard_event_logger = { workspace = true } defguard_mail = { workspace = true } defguard_proxy_manager = { workspace = true } +defguard_session_manager = { workspace = true } defguard_version = { workspace = true } # external dependencies diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index d3ed03244b..dad192dcdb 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -167,6 +167,7 @@ async fn main() -> Result<(), anyhow::Error> { wireguard_tx.clone(), mail_tx.clone(), grpc_event_tx, + peer_stats_tx, ) => error!("Gateway gRPC stream returned early: {res:?}"), res = run_grpc_server( Arc::clone(&worker_state), @@ -174,7 +175,6 @@ async fn main() -> Result<(), anyhow::Error> { grpc_cert, grpc_key, failed_logins.clone(), - peer_stats_tx, ) => error!("gRPC server returned early: {res:?}"), res = run_web_server( worker_state, diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 3e5f49b78d..09910e9c87 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -12,8 +12,8 @@ use model_derive::Model; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use sqlx::{ - FromRow, PgConnection, PgExecutor, PgPool, Type, postgres::types::PgInterval, query, query_as, - query_scalar, + Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, Type, + postgres::types::PgInterval, query, query_as, query_scalar, }; use thiserror::Error; use tracing::{debug, info}; @@ -25,11 +25,16 @@ use super::{ device::{Device, DeviceError, DeviceType, WireguardNetworkDevice}, group::{Group, Permission}, user::User, - wireguard_peer_stats::WireguardPeerStats, }; use crate::{ auth::claims::{Claims, ClaimsType}, - db::{Id, NoId}, + db::{ + Id, NoId, + models::{ + vpn_client_session::{VpnClientSession, VpnClientSessionState}, + vpn_session_stats::VpnSessionStats, + }, + }, types::user_info::UserInfo, utils::parse_address_list, }; @@ -1211,7 +1216,7 @@ pub async fn networks_stats( mod test { use std::str::FromStr; - use crate::db::setup_pool; + use crate::db::{models::wireguard_peer_stats::WireguardPeerStats, setup_pool}; use chrono::{SubsecRound, TimeDelta, Utc}; use matches::assert_matches; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 51995484f5..b9ea90a85c 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -42,9 +42,10 @@ use tonic::{ use crate::{ ClaimsType, enterprise::firewall::try_get_location_firewall_config, + events::GrpcRequestContext, grpc::{ ClientMap, GrpcEvent, TEN_SECS, - gateway::{GrpcRequestContext, events::GatewayEvent, get_peers}, + gateway::{events::GatewayEvent, get_peers}, }, handlers::mail::send_gateway_disconnected_email, }; diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index e007e2412a..9b64ab166e 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -4,6 +4,7 @@ use std::{ sync::{Arc, Mutex}, }; +use chrono::DateTime; use defguard_common::{ config::server_config, db::{ @@ -15,7 +16,7 @@ use defguard_common::{ use defguard_mail::Mail; use defguard_proto::{ enterprise::firewall::FirewallConfig, - gateway::{Configuration, CoreResponse, Peer, Update, core_response, update}, + gateway::{Configuration, CoreResponse, Peer, PeerStats, Update, core_response, update}, }; use sqlx::{PgExecutor, PgPool, postgres::PgListener, query}; use thiserror::Error; @@ -30,7 +31,7 @@ use tonic::{Code, Status}; use crate::{ enterprise::is_enterprise_license_active, - events::{GrpcEvent, GrpcRequestContext}, + events::GrpcEvent, grpc::gateway::{client_state::ClientMap, events::GatewayEvent, handler::GatewayHandler}, }; @@ -224,13 +225,14 @@ fn gen_config( const GATEWAY_TABLE_TRIGGER: &str = "gateway_change"; -/// Bi-directional gRPC stream for comminication with Defguard Gateway. +/// Bi-directional gRPC stream for communication with Defguard Gateway. pub async fn run_grpc_gateway_stream( pool: PgPool, client_state: Arc>, events_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { let config = server_config(); let tls_config = config.grpc_client_tls_config()?; @@ -814,26 +816,26 @@ impl GatewayUpdatesHandler { // } // } - // convert stats to DB storage format - match try_protos_into_stats_message(peer_stats.clone(), network_id, device_id) { - None => { - warn!( - "Failed to parse peer stats update. Skipping sending message to session manager." - ) - } - Some(message) => { - self.peer_stats_tx.send(message).map_err(|err| { - error!("Failed to send peers stats update to session manager: {err}"); - Status::new( - Code::Internal, - format!("Failed to send peers stats update to session manager: {err}"), - ) - })?; - } - }; +// convert stats to DB storage format +// match try_protos_into_stats_message(peer_stats.clone(), network_id, device_id) { +// None => { +// warn!( +// "Failed to parse peer stats update. Skipping sending message to session manager." +// ) +// } +// Some(message) => { +// self.peer_stats_tx.send(message).map_err(|err| { +// error!("Failed to send peers stats update to session manager: {err}"); +// Status::new( +// Code::Internal, +// format!("Failed to send peers stats update to session manager: {err}"), +// ) +// })?; +// } +// }; - // convert stats to DB storage format - let stats = protos_into_internal_stats(peer_stats, network_id, device_id); +// convert stats to DB storage format +// let stats = protos_into_internal_stats(peer_stats, network_id, device_id); // // emit client disconnect events // for (device, context) in disconnected_clients { diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index d65de2f34d..00fe736a22 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -70,7 +70,6 @@ pub async fn run_grpc_server( grpc_cert: Option, grpc_key: Option, failed_logins: Arc>, - peer_stats_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { // Build gRPC services let server = if let (Some(cert), Some(key)) = (grpc_cert, grpc_key) { @@ -100,7 +99,6 @@ pub async fn build_grpc_service_router( pool: PgPool, worker_state: Arc>, failed_logins: Arc>, - peer_stats_tx: UnboundedSender, // incompatible_components: Arc>, ) -> Result { let auth_service = AuthServiceServer::new(AuthServer::new(pool.clone(), failed_logins)); diff --git a/proto b/proto index 5dfc8c8d23..0db6f5cb5b 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 5dfc8c8d23ac0613108a2b7b921fd9a97613bb3a +Subproject commit 0db6f5cb5ba834abcf29dddbe4076faa7f69e363 From 2441ab06e40a363f478ad18054c1228d396796a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 9 Jan 2026 14:21:10 +0100 Subject: [PATCH 28/52] move sessions migration --- ...sions.down.sql => 20260109121935_vpn_client_sessions.down.sql} | 0 ..._sessions.up.sql => 20260109121935_vpn_client_sessions.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename migrations/{20251121121935_vpn_client_sessions.down.sql => 20260109121935_vpn_client_sessions.down.sql} (100%) rename migrations/{20251121121935_vpn_client_sessions.up.sql => 20260109121935_vpn_client_sessions.up.sql} (100%) diff --git a/migrations/20251121121935_vpn_client_sessions.down.sql b/migrations/20260109121935_vpn_client_sessions.down.sql similarity index 100% rename from migrations/20251121121935_vpn_client_sessions.down.sql rename to migrations/20260109121935_vpn_client_sessions.down.sql diff --git a/migrations/20251121121935_vpn_client_sessions.up.sql b/migrations/20260109121935_vpn_client_sessions.up.sql similarity index 100% rename from migrations/20251121121935_vpn_client_sessions.up.sql rename to migrations/20260109121935_vpn_client_sessions.up.sql From e8ff151ec31da34db94b9cfe1fa3b9173a2f1af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 13 Jan 2026 12:48:45 +0100 Subject: [PATCH 29/52] update query cache --- ...d6b30ad0914cf6ba95a25fee45e00394262a.json} | 5 +- ...ff809ae43b422343a5600fc91ac572e42fb4a.json | 35 ++++++++ ...47840e602bd006a7aec6790376325158449af.json | 70 ++++++++++++++++ ...1785d0aca658a64256c28d4b044e77f6fb603.json | 68 +++++++++++++++ ...2098903b2ab7b1ded07501f2d7ce822c8d712.json | 23 ++++++ ...63493c7e18d05dfdc0a9e8f35b44a5d1141b7.json | 33 ++++++++ ...fe14e8671fdeae40b5f82c74a2f17d8202aad.json | 14 ++++ ...51cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json | 82 +++++++++++++++++++ ...109f00658195b025e08e22962fb9c247d89d6.json | 81 ++++++++++++++++++ ...7997d76e6d10790ba642c65fa4a616a76ada.json} | 5 +- ...de853deb5d6ce935931369e29d0dbef8dabb4.json | 79 ++++++++++++++++++ ...395070838b7b5d03c68203ce51d80a255dd9.json} | 5 +- ...5eead07e52716ee284e3305bb54f6636e164.json} | 4 +- ...d3200e860a3413af82124319bbc3591625b7.json} | 5 +- ...5288d8156f5610deb126ddaa3d8855b931384.json | 14 ++++ ...849660ab5ae3c9e7057dd5c24877c3fdcedbe.json | 81 ++++++++++++++++++ ...fdeada1a1cd59bf756867fd4777e13d768283.json | 70 ++++++++++++++++ ...d98d798b879bd0c14ad1f467b51cb53391db7.json | 29 +++++++ ...f1838986d5a7e07c06e0b8ed4e556d71b7964.json | 32 ++++++++ ...f50e2abdc74addccce5b36ca53cd7f057fd56.json | 22 +++++ ...66771f7759689c60d182aaa7feb051856910c.json | 71 ++++++++++++++++ ...e9fe417a3f7f734adc81052a707e15d17ccc4.json | 40 +++++++++ ...d0633419d573e63edceb419e972c8dba451f4.json | 43 ++++++++++ 23 files changed, 898 insertions(+), 13 deletions(-) rename .sqlx/{query-ab5c925bc572cde131aad371e72158c237823dd9908ec8f02dd6f5eeabe9af3b.json => query-022c7f0f5021684b5ab726173b25d6b30ad0914cf6ba95a25fee45e00394262a.json} (53%) create mode 100644 .sqlx/query-09be8d0acda81a4090c6ab41e79ff809ae43b422343a5600fc91ac572e42fb4a.json create mode 100644 .sqlx/query-1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af.json create mode 100644 .sqlx/query-21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603.json create mode 100644 .sqlx/query-28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712.json create mode 100644 .sqlx/query-2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7.json create mode 100644 .sqlx/query-3e451dceb9123bc05bd78871d47fe14e8671fdeae40b5f82c74a2f17d8202aad.json create mode 100644 .sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json create mode 100644 .sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json rename .sqlx/{query-a8a6b28b4a4bfbd7857795ec3d58ff7d27920c68b04d325e70628954ba85f4fd.json => query-563d8713da5b19990f4bc0792ca77997d76e6d10790ba642c65fa4a616a76ada.json} (63%) create mode 100644 .sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json rename .sqlx/{query-9d11c1fcb305f13f6141c89a3309f61e8f3fcfbbaa41a988aca381a89fc66ecc.json => query-681a7ee34229f3d70955e697fafb395070838b7b5d03c68203ce51d80a255dd9.json} (85%) rename .sqlx/{query-a2816a117b4955605e4f011d4effee27e1ed4525d48ae2a73f88097796aeaf8e.json => query-6b39dd84bcf70186a063311b81845eead07e52716ee284e3305bb54f6636e164.json} (54%) rename .sqlx/{query-42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8.json => query-94ee4d592e148aeb460f17292941d3200e860a3413af82124319bbc3591625b7.json} (66%) create mode 100644 .sqlx/query-a5f1f6dee5537cfab98e1845a685288d8156f5610deb126ddaa3d8855b931384.json create mode 100644 .sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json create mode 100644 .sqlx/query-ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283.json create mode 100644 .sqlx/query-c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7.json create mode 100644 .sqlx/query-e013295d3470549d967c28fbe47f1838986d5a7e07c06e0b8ed4e556d71b7964.json create mode 100644 .sqlx/query-e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56.json create mode 100644 .sqlx/query-e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c.json create mode 100644 .sqlx/query-e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4.json create mode 100644 .sqlx/query-f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4.json diff --git a/.sqlx/query-ab5c925bc572cde131aad371e72158c237823dd9908ec8f02dd6f5eeabe9af3b.json b/.sqlx/query-022c7f0f5021684b5ab726173b25d6b30ad0914cf6ba95a25fee45e00394262a.json similarity index 53% rename from .sqlx/query-ab5c925bc572cde131aad371e72158c237823dd9908ec8f02dd6f5eeabe9af3b.json rename to .sqlx/query-022c7f0f5021684b5ab726173b25d6b30ad0914cf6ba95a25fee45e00394262a.json index 1258b2795b..1752c50a6e 100644 --- a/.sqlx/query-ab5c925bc572cde131aad371e72158c237823dd9908ec8f02dd6f5eeabe9af3b.json +++ b/.sqlx/query-022c7f0f5021684b5ab726173b25d6b30ad0914cf6ba95a25fee45e00394262a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" FROM wireguard_peer_stats s JOIN device d ON d.id = s.device_id LEFT JOIN \"user\" u ON u.id = d.user_id WHERE latest_handshake >= $1 AND s.network = $2", + "query": "SELECT COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" FROM vpn_client_session s LEFT JOIN device d ON d.id = s.device_id WHERE s.location_id = $1 AND s.state = 'connected'", "describe": { "columns": [ { @@ -21,7 +21,6 @@ ], "parameters": { "Left": [ - "Timestamp", "Int8" ] }, @@ -31,5 +30,5 @@ null ] }, - "hash": "ab5c925bc572cde131aad371e72158c237823dd9908ec8f02dd6f5eeabe9af3b" + "hash": "022c7f0f5021684b5ab726173b25d6b30ad0914cf6ba95a25fee45e00394262a" } diff --git a/.sqlx/query-09be8d0acda81a4090c6ab41e79ff809ae43b422343a5600fc91ac572e42fb4a.json b/.sqlx/query-09be8d0acda81a4090c6ab41e79ff809ae43b422343a5600fc91ac572e42fb4a.json new file mode 100644 index 0000000000..4d1786da77 --- /dev/null +++ b/.sqlx/query-09be8d0acda81a4090c6ab41e79ff809ae43b422343a5600fc91ac572e42fb4a.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" FROM vpn_client_session s LEFT JOIN device d ON d.id = s.device_id WHERE s.location_id = $1 AND (s.state = 'connected' OR (s.state = 'disconnected' AND s.disconnected_at >= $2))", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "active_users!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "active_user_devices!", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "active_network_devices!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Timestamp" + ] + }, + "nullable": [ + null, + null, + null + ] + }, + "hash": "09be8d0acda81a4090c6ab41e79ff809ae43b422343a5600fc91ac572e42fb4a" +} diff --git a/.sqlx/query-1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af.json b/.sqlx/query-1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af.json new file mode 100644 index 0000000000..d132f8e9f3 --- /dev/null +++ b/.sqlx/query-1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, \"session_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\" FROM \"vpn_session_stats\" WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "session_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "collected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 3, + "name": "latest_handshake", + "type_info": "Timestamp" + }, + { + "ordinal": 4, + "name": "endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "total_upload", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_download", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "upload_diff", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "download_diff", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af" +} diff --git a/.sqlx/query-21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603.json b/.sqlx/query-21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603.json new file mode 100644 index 0000000000..aefe121174 --- /dev/null +++ b/.sqlx/query-21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, \"session_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\" FROM \"vpn_session_stats\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "session_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "collected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 3, + "name": "latest_handshake", + "type_info": "Timestamp" + }, + { + "ordinal": 4, + "name": "endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "total_upload", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_download", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "upload_diff", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "download_diff", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603" +} diff --git a/.sqlx/query-28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712.json b/.sqlx/query-28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712.json new file mode 100644 index 0000000000..277f3af122 --- /dev/null +++ b/.sqlx/query-28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT connected_at FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND connected_at IS NOT NULL ORDER BY connected_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "connected_at", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712" +} diff --git a/.sqlx/query-2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7.json b/.sqlx/query-2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7.json new file mode 100644 index 0000000000..188ec6128f --- /dev/null +++ b/.sqlx/query-2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7.json @@ -0,0 +1,33 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"vpn_client_session\" SET \"location_id\" = $2,\"user_id\" = $3,\"device_id\" = $4,\"created_at\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"mfa\" = $8,\"state\" = $9 WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Int8", + "Timestamp", + "Timestamp", + "Timestamp", + "Bool", + { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + ] + }, + "nullable": [] + }, + "hash": "2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7" +} diff --git a/.sqlx/query-3e451dceb9123bc05bd78871d47fe14e8671fdeae40b5f82c74a2f17d8202aad.json b/.sqlx/query-3e451dceb9123bc05bd78871d47fe14e8671fdeae40b5f82c74a2f17d8202aad.json new file mode 100644 index 0000000000..968f4c9247 --- /dev/null +++ b/.sqlx/query-3e451dceb9123bc05bd78871d47fe14e8671fdeae40b5f82c74a2f17d8202aad.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM \"vpn_session_stats\" WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "3e451dceb9123bc05bd78871d47fe14e8671fdeae40b5f82c74a2f17d8202aad" +} diff --git a/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json b/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json new file mode 100644 index 0000000000..a6b7713486 --- /dev/null +++ b/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json @@ -0,0 +1,82 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa, state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND device_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "mfa", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "state: VpnClientSessionState", + "type_info": { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f" +} diff --git a/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json b/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json new file mode 100644 index 0000000000..01e95086f0 --- /dev/null +++ b/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json @@ -0,0 +1,81 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa, state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "mfa", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "state: VpnClientSessionState", + "type_info": { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6" +} diff --git a/.sqlx/query-a8a6b28b4a4bfbd7857795ec3d58ff7d27920c68b04d325e70628954ba85f4fd.json b/.sqlx/query-563d8713da5b19990f4bc0792ca77997d76e6d10790ba642c65fa4a616a76ada.json similarity index 63% rename from .sqlx/query-a8a6b28b4a4bfbd7857795ec3d58ff7d27920c68b04d325e70628954ba85f4fd.json rename to .sqlx/query-563d8713da5b19990f4bc0792ca77997d76e6d10790ba642c65fa4a616a76ada.json index d63288fac8..6af2e5e8ea 100644 --- a/.sqlx/query-a8a6b28b4a4bfbd7857795ec3d58ff7d27920c68b04d325e70628954ba85f4fd.json +++ b/.sqlx/query-563d8713da5b19990f4bc0792ca77997d76e6d10790ba642c65fa4a616a76ada.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download FROM wireguard_peer_stats_view WHERE collected_at >= $2 GROUP BY 1 ORDER BY 1 LIMIT $3", + "query": "SELECT date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", cast(sum(upload_diff) AS bigint) upload, cast(sum(download_diff) AS bigint) download FROM vpn_session_stats JOIN vpn_client_session s ON session_id = s.id WHERE collected_at >= $2 AND s.location_id = $3 GROUP BY 1 ORDER BY 1 LIMIT $4", "describe": { "columns": [ { @@ -23,6 +23,7 @@ "Left": [ "Text", "Timestamp", + "Int8", "Int8" ] }, @@ -32,5 +33,5 @@ null ] }, - "hash": "a8a6b28b4a4bfbd7857795ec3d58ff7d27920c68b04d325e70628954ba85f4fd" + "hash": "563d8713da5b19990f4bc0792ca77997d76e6d10790ba642c65fa4a616a76ada" } diff --git a/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json b/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json new file mode 100644 index 0000000000..1daf28db36 --- /dev/null +++ b/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json @@ -0,0 +1,79 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa\",\"state\" \"state: _\" FROM \"vpn_client_session\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "mfa", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "state: _", + "type_info": { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4" +} diff --git a/.sqlx/query-9d11c1fcb305f13f6141c89a3309f61e8f3fcfbbaa41a988aca381a89fc66ecc.json b/.sqlx/query-681a7ee34229f3d70955e697fafb395070838b7b5d03c68203ce51d80a255dd9.json similarity index 85% rename from .sqlx/query-9d11c1fcb305f13f6141c89a3309f61e8f3fcfbbaa41a988aca381a89fc66ecc.json rename to .sqlx/query-681a7ee34229f3d70955e697fafb395070838b7b5d03c68203ce51d80a255dd9.json index c10a5cf530..5982e87c63 100644 --- a/.sqlx/query-9d11c1fcb305f13f6141c89a3309f61e8f3fcfbbaa41a988aca381a89fc66ecc.json +++ b/.sqlx/query-681a7ee34229f3d70955e697fafb395070838b7b5d03c68203ce51d80a255dd9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT DISTINCT ON (d.id) d.id, d.name, d.wireguard_pubkey, d.user_id, d.created, d.description, d.device_type \"device_type: DeviceType\", d.configured FROM device d JOIN wireguard_peer_stats s ON d.id = s.device_id WHERE s.latest_handshake >= $1 AND s.network = $2 AND d.device_type = $3", + "query": "SELECT DISTINCT ON (d.id) d.id, d.name, d.wireguard_pubkey, d.user_id, d.created, d.description, d.device_type \"device_type: DeviceType\", d.configured FROM device d JOIN vpn_client_session s ON d.id = s.device_id WHERE s.state = 'connected' AND s.location_id = $1 AND d.device_type = $2", "describe": { "columns": [ { @@ -56,7 +56,6 @@ ], "parameters": { "Left": [ - "Timestamp", "Int8", { "Custom": { @@ -82,5 +81,5 @@ false ] }, - "hash": "9d11c1fcb305f13f6141c89a3309f61e8f3fcfbbaa41a988aca381a89fc66ecc" + "hash": "681a7ee34229f3d70955e697fafb395070838b7b5d03c68203ce51d80a255dd9" } diff --git a/.sqlx/query-a2816a117b4955605e4f011d4effee27e1ed4525d48ae2a73f88097796aeaf8e.json b/.sqlx/query-6b39dd84bcf70186a063311b81845eead07e52716ee284e3305bb54f6636e164.json similarity index 54% rename from .sqlx/query-a2816a117b4955605e4f011d4effee27e1ed4525d48ae2a73f88097796aeaf8e.json rename to .sqlx/query-6b39dd84bcf70186a063311b81845eead07e52716ee284e3305bb54f6636e164.json index efaf8aa6a9..c80bcbcf64 100644 --- a/.sqlx/query-a2816a117b4955605e4f011d4effee27e1ed4525d48ae2a73f88097796aeaf8e.json +++ b/.sqlx/query-6b39dd84bcf70186a063311b81845eead07e52716ee284e3305bb54f6636e164.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN u.id END), 0) \"active_users!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" FROM wireguard_peer_stats s JOIN device d ON d.id = s.device_id LEFT JOIN \"user\" u ON u.id = d.user_id WHERE latest_handshake >= $1", + "query": "SELECT COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" FROM vpn_client_session s LEFT JOIN device d ON d.id = s.device_id WHERE s.state = 'connected' OR (s.state = 'disconnected' AND s.disconnected_at >= $1)", "describe": { "columns": [ { @@ -30,5 +30,5 @@ null ] }, - "hash": "a2816a117b4955605e4f011d4effee27e1ed4525d48ae2a73f88097796aeaf8e" + "hash": "6b39dd84bcf70186a063311b81845eead07e52716ee284e3305bb54f6636e164" } diff --git a/.sqlx/query-42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8.json b/.sqlx/query-94ee4d592e148aeb460f17292941d3200e860a3413af82124319bbc3591625b7.json similarity index 66% rename from .sqlx/query-42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8.json rename to .sqlx/query-94ee4d592e148aeb460f17292941d3200e860a3413af82124319bbc3591625b7.json index 8b795c04aa..dc946b0a3a 100644 --- a/.sqlx/query-42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8.json +++ b/.sqlx/query-94ee4d592e148aeb460f17292941d3200e860a3413af82124319bbc3591625b7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", cast(sum(upload) AS bigint) upload, cast(sum(download) AS bigint) download FROM wireguard_peer_stats_view WHERE collected_at >= $2 AND network = $3 GROUP BY 1 ORDER BY 1 LIMIT $4", + "query": "SELECT date_trunc($1, collected_at) \"collected_at: NaiveDateTime\", cast(sum(upload_diff) AS bigint) upload, cast(sum(download_diff) AS bigint) download FROM vpn_session_stats JOIN vpn_client_session s ON session_id = s.id WHERE collected_at >= $2 GROUP BY 1 ORDER BY 1 LIMIT $3", "describe": { "columns": [ { @@ -23,7 +23,6 @@ "Left": [ "Text", "Timestamp", - "Int8", "Int8" ] }, @@ -33,5 +32,5 @@ null ] }, - "hash": "42ccaa218d47638ff39d9006095ac30ae1cd9dce74ec826ed875c39cc05f04f8" + "hash": "94ee4d592e148aeb460f17292941d3200e860a3413af82124319bbc3591625b7" } diff --git a/.sqlx/query-a5f1f6dee5537cfab98e1845a685288d8156f5610deb126ddaa3d8855b931384.json b/.sqlx/query-a5f1f6dee5537cfab98e1845a685288d8156f5610deb126ddaa3d8855b931384.json new file mode 100644 index 0000000000..dcf8f1792c --- /dev/null +++ b/.sqlx/query-a5f1f6dee5537cfab98e1845a685288d8156f5610deb126ddaa3d8855b931384.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM \"vpn_client_session\" WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "a5f1f6dee5537cfab98e1845a685288d8156f5610deb126ddaa3d8855b931384" +} diff --git a/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json b/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json new file mode 100644 index 0000000000..74a4381829 --- /dev/null +++ b/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json @@ -0,0 +1,81 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa\",\"state\" \"state: _\" FROM \"vpn_client_session\" WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "mfa", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "state: _", + "type_info": { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe" +} diff --git a/.sqlx/query-ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283.json b/.sqlx/query-ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283.json new file mode 100644 index 0000000000..340dcda4cf --- /dev/null +++ b/.sqlx/query-ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, session_id, collected_at, latest_handshake, endpoint, total_upload, total_download, upload_diff, download_diff\n \tFROM vpn_session_stats WHERE session_id = $1 ORDER BY collected_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "session_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "collected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 3, + "name": "latest_handshake", + "type_info": "Timestamp" + }, + { + "ordinal": 4, + "name": "endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "total_upload", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_download", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "upload_diff", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "download_diff", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283" +} diff --git a/.sqlx/query-c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7.json b/.sqlx/query-c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7.json new file mode 100644 index 0000000000..ab1863bd71 --- /dev/null +++ b/.sqlx/query-c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"vpn_session_stats\" (\"session_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Timestamp", + "Timestamp", + "Text", + "Int8", + "Int8", + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7" +} diff --git a/.sqlx/query-e013295d3470549d967c28fbe47f1838986d5a7e07c06e0b8ed4e556d71b7964.json b/.sqlx/query-e013295d3470549d967c28fbe47f1838986d5a7e07c06e0b8ed4e556d71b7964.json new file mode 100644 index 0000000000..77db208cf0 --- /dev/null +++ b/.sqlx/query-e013295d3470549d967c28fbe47f1838986d5a7e07c06e0b8ed4e556d71b7964.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN s.user_id END), 0) \"active_users!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'user' THEN d.id END), 0) \"active_user_devices!\", COALESCE(COUNT(DISTINCT CASE WHEN d.device_type = 'network' THEN d.id END), 0) \"active_network_devices!\" FROM vpn_client_session s LEFT JOIN device d ON d.id = s.device_id WHERE s.state = 'connected'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "active_users!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "active_user_devices!", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "active_network_devices!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null, + null, + null + ] + }, + "hash": "e013295d3470549d967c28fbe47f1838986d5a7e07c06e0b8ed4e556d71b7964" +} diff --git a/.sqlx/query-e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56.json b/.sqlx/query-e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56.json new file mode 100644 index 0000000000..c4d19afc5a --- /dev/null +++ b/.sqlx/query-e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"vpn_session_stats\" SET \"session_id\" = $2,\"collected_at\" = $3,\"latest_handshake\" = $4,\"endpoint\" = $5,\"total_upload\" = $6,\"total_download\" = $7,\"upload_diff\" = $8,\"download_diff\" = $9 WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Timestamp", + "Timestamp", + "Text", + "Int8", + "Int8", + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56" +} diff --git a/.sqlx/query-e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c.json b/.sqlx/query-e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c.json new file mode 100644 index 0000000000..875ff12d94 --- /dev/null +++ b/.sqlx/query-e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c.json @@ -0,0 +1,71 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT st.id, session_id, collected_at, latest_handshake, endpoint, total_upload, total_download, upload_diff, download_diff FROM vpn_session_stats st JOIN vpn_client_session se ON session_id = se.id WHERE device_id = $1 AND location_id = $2 ORDER BY collected_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "session_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "collected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 3, + "name": "latest_handshake", + "type_info": "Timestamp" + }, + { + "ordinal": 4, + "name": "endpoint", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "total_upload", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "total_download", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "upload_diff", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "download_diff", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c" +} diff --git a/.sqlx/query-e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4.json b/.sqlx/query-e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4.json new file mode 100644 index 0000000000..3d81f088d8 --- /dev/null +++ b/.sqlx/query-e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"vpn_client_session\" (\"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa\",\"state\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Timestamp", + "Timestamp", + "Timestamp", + "Bool", + { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + ] + }, + "nullable": [ + false + ] + }, + "hash": "e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4" +} diff --git a/.sqlx/query-f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4.json b/.sqlx/query-f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4.json new file mode 100644 index 0000000000..481cb9a635 --- /dev/null +++ b/.sqlx/query-f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4.json @@ -0,0 +1,43 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT s.device_id \"device_id!\", date_trunc($1, collected_at) \"collected_at!: NaiveDateTime\", CAST(sum(download_diff) AS bigint) \"download!\", CAST(sum(upload_diff) AS bigint) \"upload!\" FROM vpn_session_stats INNER JOIN vpn_client_session s ON session_id = s.id WHERE s.device_id = ANY($2) AND collected_at >= $3 AND s.location_id = $4 GROUP BY device_id, collected_at ORDER BY device_id, collected_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "device_id!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "collected_at!: NaiveDateTime", + "type_info": "Timestamp" + }, + { + "ordinal": 2, + "name": "download!", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "upload!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8Array", + "Timestamp", + "Int8" + ] + }, + "nullable": [ + true, + null, + null, + null + ] + }, + "hash": "f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4" +} From e7eeed85b45cb5664694cacd84e56f0ccd61ed71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 15 Jan 2026 11:32:39 +0100 Subject: [PATCH 30/52] update submodule --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 0db6f5cb5b..161c6c6776 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 0db6f5cb5ba834abcf29dddbe4076faa7f69e363 +Subproject commit 161c6c677662130924e8bac0c16421b8ed085d33 From e9f8865da43e0a43e6b49905258a247a34eb3ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 15 Jan 2026 11:32:47 +0100 Subject: [PATCH 31/52] rename migrations --- ...wn.sql => 20260115121935_[2.0.0]_vpn_client_sessions.down.sql} | 0 ...s.up.sql => 20260115121935_[2.0.0]_vpn_client_sessions.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename migrations/{20260109121935_vpn_client_sessions.down.sql => 20260115121935_[2.0.0]_vpn_client_sessions.down.sql} (100%) rename migrations/{20260109121935_vpn_client_sessions.up.sql => 20260115121935_[2.0.0]_vpn_client_sessions.up.sql} (100%) diff --git a/migrations/20260109121935_vpn_client_sessions.down.sql b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.down.sql similarity index 100% rename from migrations/20260109121935_vpn_client_sessions.down.sql rename to migrations/20260115121935_[2.0.0]_vpn_client_sessions.down.sql diff --git a/migrations/20260109121935_vpn_client_sessions.up.sql b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql similarity index 100% rename from migrations/20260109121935_vpn_client_sessions.up.sql rename to migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql From 19035208d61ba41e7edc4662ae1c2a87ae524776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 15 Jan 2026 11:41:41 +0100 Subject: [PATCH 32/52] post-merge fixes --- crates/defguard/src/main.rs | 6 ++---- crates/defguard_common/src/db/models/wireguard.rs | 1 - crates/defguard_core/src/grpc/gateway/handler.rs | 2 +- crates/defguard_core/src/grpc/mod.rs | 5 ----- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index db3e127487..ac6e2f75c1 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -10,10 +10,8 @@ use defguard_common::{ db::{ init_db, models::{ - Settings, - User, + Settings, User, settings::{initialize_current_settings, update_current_settings}, - // wireguard_peer_stats::WireguardPeerStats, }, }, messages::peer_stats_update::PeerStatsUpdate, @@ -42,7 +40,7 @@ use defguard_event_logger::{message::EventLoggerMessage, run_event_logger}; use defguard_event_router::{RouterReceiverSet, run_event_router}; use defguard_mail::{Mail, run_mail_handler}; use defguard_proxy_manager::{ProxyManager, ProxyTxSet}; -// use defguard_session_manager::run_session_manager; +use defguard_session_manager::run_session_manager; use secrecy::ExposeSecret; use tokio::sync::{broadcast, mpsc::unbounded_channel}; diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index beffdd81d9..09910e9c87 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -1222,7 +1222,6 @@ mod test { use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use crate::db::setup_pool; #[sqlx::test] async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 091511528b..5102495039 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -43,7 +43,7 @@ use crate::{ events::GrpcRequestContext, grpc::{ ClientMap, GrpcEvent, TEN_SECS, - gateway::{GatewayError, GrpcRequestContext, events::GatewayEvent, get_peers}, + gateway::{GatewayError, events::GatewayEvent, get_peers}, }, handlers::mail::send_gateway_disconnected_email, }; diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index ddc1462bdc..73edecd08d 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -5,10 +5,6 @@ use std::{ time::{Duration, Instant}, }; -use defguard_common::{ - auth::claims::ClaimsType, - db::{Id, models::Settings}, -}; use reqwest::Url; use serde::Serialize; use sqlx::PgPool; @@ -18,7 +14,6 @@ use tonic::transport::{Identity, Server, ServerTlsConfig, server::Router}; use defguard_common::{ auth::claims::ClaimsType, db::{Id, models::Settings}, - messages::peer_stats_update::PeerStatsUpdate, }; use self::{auth::AuthServer, interceptor::JwtInterceptor, worker::WorkerServer}; From bbf548f47ebb28787b4033d63b6506672cad57a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 15 Jan 2026 11:42:37 +0100 Subject: [PATCH 33/52] remove unused method --- .../src/db/models/wireguard.rs | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 09910e9c87..281034fe00 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -481,43 +481,6 @@ impl WireguardNetwork { self.address.iter().find(|net| net.contains(addr)).copied() } - /// Finds when the device connected based on handshake timestamps. - async fn connected_at( - &self, - conn: &PgPool, - device_id: Id, - ) -> Result, sqlx::Error> { - // Find a first handshake gap longer than WIREGUARD_MAX_HANDSHAKE. - // We assume that this gap indicates a time when the device was not connected. - // So, the handshake after this gap is the moment the last connection was established. - // If no such gap is found, the device may be connected from the beginning, return the first - // handshake in this case. - let connected_at = query_scalar!( - "WITH stats AS \ - (SELECT * FROM wireguard_peer_stats_view WHERE device_id = $1 AND network = $2) \ - SELECT \ - COALESCE( \ - ( \ - SELECT latest_handshake \"latest_handshake: NaiveDateTime\" \ - FROM stats WHERE latest_handshake_diff > $3 \ - ORDER BY collected_at DESC LIMIT 1 \ - ), \ - ( \ - SELECT latest_handshake \"latest_handshake: NaiveDateTime\" \ - FROM stats ORDER BY collected_at LIMIT 1 \ - ) \ - ) \ - AS latest_handshake", - device_id, - self.id, - PgInterval::try_from(WIREGUARD_MAX_HANDSHAKE).unwrap() - ) - .fetch_one(conn) - .await?; - - Ok(connected_at) - } - /// Update `connected_at` to the current time and save it to the database. pub async fn touch_connected<'e, E>(&mut self, executor: E) -> Result<(), sqlx::Error> where From 9a2eb0a0f1d9701b513b30b39cb751962c857d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 15 Jan 2026 16:40:15 +0100 Subject: [PATCH 34/52] fix existing tests --- .../src/db/models/wireguard.rs | 28 ++++++++--------- .../defguard_core/src/grpc/gateway/handler.rs | 31 +++++++++++++++++-- crates/defguard_core/src/grpc/gateway/mod.rs | 4 +-- .../src/session_state.rs | 7 ++--- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 281034fe00..65ee85f5ac 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -12,8 +12,8 @@ use model_derive::Model; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use sqlx::{ - Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, Type, - postgres::types::PgInterval, query, query_as, query_scalar, + Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, Type, query, query_as, + query_scalar, }; use thiserror::Error; use tracing::{debug, info}; @@ -1189,9 +1189,9 @@ mod test { #[sqlx::test] async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let mut network = WireguardNetwork::default(); - network.try_set_address("10.1.1.1/29").unwrap(); - let network = network.save(&pool).await.unwrap(); + let mut location = WireguardNetwork::default(); + location.try_set_address("10.1.1.1/29").unwrap(); + let location = location.save(&pool).await.unwrap(); let user = User::new( "testuser", @@ -1226,7 +1226,7 @@ mod test { id: NoId, device_id: device.id, collected_at: now - TimeDelta::minutes(i), - network: network.id, + network: location.id, endpoint: Some("11.22.33.44".into()), upload: (samples - i) * 10, download: (samples - i) * 20, @@ -1238,8 +1238,8 @@ mod test { .unwrap(); } - let connected_at = network - .connected_at(&pool, device.id) + let connected_at = device + .last_connected_at(&pool, location.id) .await .unwrap() .unwrap(); @@ -1253,9 +1253,9 @@ mod test { #[sqlx::test] async fn test_connected_at_always_connected(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let mut network = WireguardNetwork::default(); - network.try_set_address("10.1.1.1/29").unwrap(); - let network = network.save(&pool).await.unwrap(); + let mut location = WireguardNetwork::default(); + location.try_set_address("10.1.1.1/29").unwrap(); + let location = location.save(&pool).await.unwrap(); let user = User::new( "testuser", @@ -1288,7 +1288,7 @@ mod test { id: NoId, device_id: device.id, collected_at: now - TimeDelta::minutes(i), - network: network.id, + network: location.id, endpoint: Some("11.22.33.44".into()), upload: (samples - i) * 10, download: (samples - i) * 20, @@ -1300,8 +1300,8 @@ mod test { .unwrap(); } - let connected_at = network - .connected_at(&pool, device.id) + let connected_at = device + .last_connected_at(&pool, location.id) .await .unwrap() .unwrap(); diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 5102495039..d279f47f85 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -18,6 +18,7 @@ use defguard_common::{ wireguard_peer_stats::WireguardPeerStats, }, }, + messages::peer_stats_update::PeerStatsUpdate, }; use defguard_mail::Mail; use defguard_proto::gateway::{ @@ -43,7 +44,7 @@ use crate::{ events::GrpcRequestContext, grpc::{ ClientMap, GrpcEvent, TEN_SECS, - gateway::{GatewayError, events::GatewayEvent, get_peers}, + gateway::{GatewayError, events::GatewayEvent, get_peers, try_protos_into_stats_message}, }, handlers::mail::send_gateway_disconnected_email, }; @@ -95,6 +96,7 @@ pub(crate) struct GatewayHandler { events_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, } impl GatewayHandler { @@ -105,6 +107,7 @@ impl GatewayHandler { events_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, + peer_stats_tx: UnboundedSender, ) -> Result { let url = Url::from_str(&gateway.url).map_err(|err| { GatewayError::EndpointError(format!( @@ -122,6 +125,7 @@ impl GatewayHandler { events_tx, mail_tx, grpc_event_tx, + peer_stats_tx, }) } @@ -516,7 +520,7 @@ impl GatewayHandler { if !config_sent { warn!( "Ignoring peer statistics from {} because it hasn't \ - authorize itself", + authorized itself", self.gateway ); continue; @@ -554,7 +558,7 @@ impl GatewayHandler { // Convert stats to database storage format. let stats = peer_stats_from_proto( - peer_stats, + peer_stats.clone(), self.gateway.network_id, device_id, ); @@ -659,6 +663,27 @@ impl GatewayHandler { } } + // convert stats to DB storage format + match try_protos_into_stats_message( + peer_stats.clone(), + self.gateway.network_id, + device_id, + ) { + None => { + warn!( + "Failed to parse peer stats update. Skipping sending message to session manager." + ) + } + Some(message) => { + if let Err(err) = self.peer_stats_tx.send(message) { + error!( + "Failed to send peers stats update to session manager: {err}" + ); + continue; + }; + } + }; + // Save stats to database. let stats = match stats.save(&self.pool).await { Ok(stats) => stats, diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 4de3e2a062..e35d265660 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -7,7 +7,6 @@ use std::{ use chrono::DateTime; use defguard_common::{ - config::server_config, db::{ ChangeNotification, Id, TriggerOperation, models::{WireguardNetwork, gateway::Gateway, wireguard::ServiceLocationMode}, @@ -32,7 +31,7 @@ use tonic::{Code, Status}; use crate::{ enterprise::{firewall::FirewallError, is_enterprise_license_active}, - events::{GrpcEvent, GrpcRequestContext}, + events::GrpcEvent, grpc::gateway::{client_state::ClientMap, events::GatewayEvent, handler::GatewayHandler}, }; @@ -268,6 +267,7 @@ pub async fn run_grpc_gateway_stream( events_tx.clone(), mail_tx.clone(), grpc_event_tx.clone(), + peer_stats_tx.clone(), )?; let abort_handle = tasks.spawn(async move { loop { diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index 425e7cc89d..8e03c0200b 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -62,16 +62,14 @@ impl From> for LastStatsUpdate { /// State of a specific VPN client session pub(crate) struct SessionState { session_id: Id, - user_id: Id, last_stats_update: Option, } impl SessionState { - fn new(session_id: Id, user_id: Id) -> Self { + fn new(session_id: Id) -> Self { Self { session_id, last_stats_update: None, - user_id, } } @@ -121,7 +119,6 @@ impl From<&VpnClientSession> for SessionState { fn from(value: &VpnClientSession) -> Self { Self { session_id: value.id, - user_id: value.user_id, last_stats_update: None, } } @@ -255,7 +252,7 @@ impl ActiveSessionsMap { .await?; // add to session map - let session_state = SessionState::new(session.id, user.id); + let session_state = SessionState::new(session.id); let session_map = self.get_or_create_location_session_map(location_id); let maybe_existing_session = session_map.insert(device_id, session_state); // if a session exists already there was an error in earlier logic From 85bcd0c2f62004d000164a7556bcf262379ff784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 16 Jan 2026 08:55:43 +0100 Subject: [PATCH 35/52] comment out tests for now --- .../src/db/models/wireguard.rs | 252 +++++++++--------- 1 file changed, 127 insertions(+), 125 deletions(-) diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 65ee85f5ac..aa35134064 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -1186,131 +1186,133 @@ mod test { use super::*; - #[sqlx::test] - async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut location = WireguardNetwork::default(); - location.try_set_address("10.1.1.1/29").unwrap(); - let location = location.save(&pool).await.unwrap(); - - let user = User::new( - "testuser", - Some("hunter2"), - "Tester", - "Test", - "test@test.com", - None, - ) - .save(&pool) - .await - .unwrap(); - let device = Device::new( - String::new(), - String::new(), - user.id, - DeviceType::User, - None, - true, - ) - .save(&pool) - .await - .unwrap(); - - // insert stats - let samples = 60; // 1 hour of samples - let now = Utc::now().naive_utc(); - for i in 0..=samples { - // simulate connection 30 minutes ago - let handshake_minutes = i * if i < 31 { 1 } else { 10 }; - WireguardPeerStats { - id: NoId, - device_id: device.id, - collected_at: now - TimeDelta::minutes(i), - network: location.id, - endpoint: Some("11.22.33.44".into()), - upload: (samples - i) * 10, - download: (samples - i) * 20, - latest_handshake: now - TimeDelta::minutes(handshake_minutes), - allowed_ips: Some("10.1.1.0/24".into()), - } - .save(&pool) - .await - .unwrap(); - } - - let connected_at = device - .last_connected_at(&pool, location.id) - .await - .unwrap() - .unwrap(); - assert_eq!( - connected_at, - // PostgreSQL stores 6 sub-second digits while chrono stores 9. - (now - TimeDelta::minutes(30)).trunc_subsecs(6), - ); - } - - #[sqlx::test] - async fn test_connected_at_always_connected(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut location = WireguardNetwork::default(); - location.try_set_address("10.1.1.1/29").unwrap(); - let location = location.save(&pool).await.unwrap(); - - let user = User::new( - "testuser", - Some("hunter2"), - "Tester", - "Test", - "test@test.com", - None, - ) - .save(&pool) - .await - .unwrap(); - let device = Device::new( - String::new(), - String::new(), - user.id, - DeviceType::User, - None, - true, - ) - .save(&pool) - .await - .unwrap(); - - // insert stats - let samples = 60; // 1 hour of samples - let now = Utc::now().naive_utc(); - for i in 0..=samples { - WireguardPeerStats { - id: NoId, - device_id: device.id, - collected_at: now - TimeDelta::minutes(i), - network: location.id, - endpoint: Some("11.22.33.44".into()), - upload: (samples - i) * 10, - download: (samples - i) * 20, - latest_handshake: now - TimeDelta::minutes(i), // handshake every minute - allowed_ips: Some("10.1.1.0/24".into()), - } - .save(&pool) - .await - .unwrap(); - } - - let connected_at = device - .last_connected_at(&pool, location.id) - .await - .unwrap() - .unwrap(); - assert_eq!( - connected_at, - // PostgreSQL stores 6 sub-second digits while chrono stores 9. - (now - TimeDelta::minutes(samples)).trunc_subsecs(6), - ); - } + // FIXME: test new connection logic + // #[sqlx::test] + // async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { + // let pool = setup_pool(options).await; + // let mut location = WireguardNetwork::default(); + // location.try_set_address("10.1.1.1/29").unwrap(); + // let location = location.save(&pool).await.unwrap(); + + // let user = User::new( + // "testuser", + // Some("hunter2"), + // "Tester", + // "Test", + // "test@test.com", + // None, + // ) + // .save(&pool) + // .await + // .unwrap(); + // let device = Device::new( + // String::new(), + // String::new(), + // user.id, + // DeviceType::User, + // None, + // true, + // ) + // .save(&pool) + // .await + // .unwrap(); + + // // insert stats + // let samples = 60; // 1 hour of samples + // let now = Utc::now().naive_utc(); + // for i in 0..=samples { + // // simulate connection 30 minutes ago + // let handshake_minutes = i * if i < 31 { 1 } else { 10 }; + // WireguardPeerStats { + // id: NoId, + // device_id: device.id, + // collected_at: now - TimeDelta::minutes(i), + // network: location.id, + // endpoint: Some("11.22.33.44".into()), + // upload: (samples - i) * 10, + // download: (samples - i) * 20, + // latest_handshake: now - TimeDelta::minutes(handshake_minutes), + // allowed_ips: Some("10.1.1.0/24".into()), + // } + // .save(&pool) + // .await + // .unwrap(); + // } + + // let connected_at = device + // .last_connected_at(&pool, location.id) + // .await + // .unwrap() + // .unwrap(); + // assert_eq!( + // connected_at, + // // PostgreSQL stores 6 sub-second digits while chrono stores 9. + // (now - TimeDelta::minutes(30)).trunc_subsecs(6), + // ); + // } + + // FIXME: test new connection logic + // #[sqlx::test] + // async fn test_connected_at_always_connected(_: PgPoolOptions, options: PgConnectOptions) { + // let pool = setup_pool(options).await; + // let mut location = WireguardNetwork::default(); + // location.try_set_address("10.1.1.1/29").unwrap(); + // let location = location.save(&pool).await.unwrap(); + + // let user = User::new( + // "testuser", + // Some("hunter2"), + // "Tester", + // "Test", + // "test@test.com", + // None, + // ) + // .save(&pool) + // .await + // .unwrap(); + // let device = Device::new( + // String::new(), + // String::new(), + // user.id, + // DeviceType::User, + // None, + // true, + // ) + // .save(&pool) + // .await + // .unwrap(); + + // // insert stats + // let samples = 60; // 1 hour of samples + // let now = Utc::now().naive_utc(); + // for i in 0..=samples { + // WireguardPeerStats { + // id: NoId, + // device_id: device.id, + // collected_at: now - TimeDelta::minutes(i), + // network: location.id, + // endpoint: Some("11.22.33.44".into()), + // upload: (samples - i) * 10, + // download: (samples - i) * 20, + // latest_handshake: now - TimeDelta::minutes(i), // handshake every minute + // allowed_ips: Some("10.1.1.0/24".into()), + // } + // .save(&pool) + // .await + // .unwrap(); + // } + + // let connected_at = device + // .last_connected_at(&pool, location.id) + // .await + // .unwrap() + // .unwrap(); + // assert_eq!( + // connected_at, + // // PostgreSQL stores 6 sub-second digits while chrono stores 9. + // (now - TimeDelta::minutes(samples)).trunc_subsecs(6), + // ); + // } #[sqlx::test] async fn test_get_allowed_devices_for_user(_: PgPoolOptions, options: PgConnectOptions) { From f4fabd0ce2576656b5b722ef8f143817bfa03781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 16 Jan 2026 08:59:19 +0100 Subject: [PATCH 36/52] add foreign key to GW table --- .../20260115121935_[2.0.0]_vpn_client_sessions.up.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql index 9914cdcc88..f65befa775 100644 --- a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql +++ b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql @@ -8,7 +8,7 @@ CREATE TABLE vpn_client_session ( id bigserial PRIMARY KEY, location_id bigint NOT NULL, user_id bigint NOT NULL, - device_id bigint NULL, + device_id bigint NOT NULL, created_at timestamp without time zone NOT NULL DEFAULT current_timestamp, connected_at timestamp without time zone NOT NULL, disconnected_at timestamp without time zone NOT NULL, @@ -22,6 +22,7 @@ CREATE TABLE vpn_client_session ( CREATE TABLE vpn_session_stats ( id bigserial PRIMARY KEY, session_id bigint NOT NULL, + gateway_id bigint NOT NULL, collected_at timestamp without time zone NOT NULL, latest_handshake timestamp without time zone NOT NULL, endpoint text NOT NULL, @@ -29,5 +30,6 @@ CREATE TABLE vpn_session_stats ( total_download bigint NOT NULL, upload_diff bigint NOT NULL, download_diff bigint NOT NULL, - FOREIGN KEY (session_id) REFERENCES vpn_client_session(id) ON DELETE CASCADE + FOREIGN KEY (session_id) REFERENCES vpn_client_session(id) ON DELETE CASCADE, + FOREIGN KEY (gateway_id) REFERENCES gateway(id) ON DELETE CASCADE ); From 2f6eeab9159f23ec1142942d1dca0d0259471338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 16 Jan 2026 09:14:59 +0100 Subject: [PATCH 37/52] make device id non-optional and add gateway id --- crates/defguard_common/src/db/models/vpn_client_session.rs | 7 +++---- crates/defguard_common/src/db/models/vpn_session_stats.rs | 5 ++++- crates/defguard_common/src/db/models/wireguard.rs | 2 +- crates/defguard_common/src/messages/peer_stats_update.rs | 3 +++ crates/defguard_core/src/grpc/gateway/handler.rs | 1 + crates/defguard_core/src/grpc/gateway/mod.rs | 2 ++ crates/defguard_session_manager/src/session_state.rs | 1 + .../20260115121935_[2.0.0]_vpn_client_sessions.up.sql | 2 +- 8 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index 45173ec44c..883f70b761 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -20,8 +20,7 @@ pub struct VpnClientSession { pub id: I, pub location_id: Id, pub user_id: Id, - // users can delete their device, but we want to retain sessions & stats - pub device_id: Option, + pub device_id: Id, pub created_at: NaiveDateTime, pub connected_at: Option, pub disconnected_at: Option, @@ -49,7 +48,7 @@ impl VpnClientSession { id: NoId, location_id, user_id, - device_id: Some(device_id), + device_id, created_at: Utc::now().naive_utc(), connected_at, disconnected_at: None, @@ -87,7 +86,7 @@ impl VpnClientSession { ) -> Result>, SqlxError> { query_as!( VpnSessionStats, - "SELECT id, session_id, collected_at, latest_handshake, endpoint, \ + "SELECT id, session_id, gateway_id, collected_at, latest_handshake, endpoint, \ total_upload, total_download, upload_diff, download_diff FROM vpn_session_stats \ WHERE session_id = $1 \ diff --git a/crates/defguard_common/src/db/models/vpn_session_stats.rs b/crates/defguard_common/src/db/models/vpn_session_stats.rs index 302a4e301d..67b732446d 100644 --- a/crates/defguard_common/src/db/models/vpn_session_stats.rs +++ b/crates/defguard_common/src/db/models/vpn_session_stats.rs @@ -9,6 +9,7 @@ use crate::db::{Id, NoId}; pub struct VpnSessionStats { pub id: I, pub session_id: Id, + pub gateway_id: Id, pub collected_at: NaiveDateTime, // handshake must have occured for a session to be considered active pub latest_handshake: NaiveDateTime, @@ -26,6 +27,7 @@ pub struct VpnSessionStats { impl VpnSessionStats { pub fn new( session_id: Id, + gateway_id: Id, collected_at: NaiveDateTime, latest_handshake: NaiveDateTime, endpoint: String, @@ -37,6 +39,7 @@ impl VpnSessionStats { Self { id: NoId, session_id, + gateway_id, collected_at, latest_handshake, endpoint, @@ -57,7 +60,7 @@ impl VpnSessionStats { ) -> Result, sqlx::Error> { let maybe_stats = query_as!( Self, - "SELECT st.id, session_id, collected_at, latest_handshake, endpoint, \ + "SELECT st.id, session_id, gateway_id, collected_at, latest_handshake, endpoint, \ total_upload, total_download, upload_diff, download_diff \ FROM vpn_session_stats st \ JOIN vpn_client_session se ON session_id = se.id \ diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index aa35134064..91be8c9930 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -514,7 +514,7 @@ impl WireguardNetwork { let stats = query_as!( WireguardDeviceTransferRow, - "SELECT s.device_id \"device_id!\", date_trunc($1, collected_at) \"collected_at!: NaiveDateTime\", \ + "SELECT s.device_id, date_trunc($1, collected_at) \"collected_at!: NaiveDateTime\", \ CAST(sum(download_diff) AS bigint) \"download!\", CAST(sum(upload_diff) AS bigint) \"upload!\" \ FROM vpn_session_stats \ INNER JOIN vpn_client_session s ON session_id = s.id \ diff --git a/crates/defguard_common/src/messages/peer_stats_update.rs b/crates/defguard_common/src/messages/peer_stats_update.rs index 3ba219387b..9db662bd35 100644 --- a/crates/defguard_common/src/messages/peer_stats_update.rs +++ b/crates/defguard_common/src/messages/peer_stats_update.rs @@ -9,6 +9,7 @@ use crate::db::Id; #[derive(Debug)] pub struct PeerStatsUpdate { pub location_id: Id, + pub gateway_id: Id, pub device_id: Id, pub collected_at: NaiveDateTime, pub endpoint: SocketAddr, @@ -22,6 +23,7 @@ pub struct PeerStatsUpdate { impl PeerStatsUpdate { pub fn new( location_id: Id, + gateway_id: Id, device_id: Id, endpoint: SocketAddr, upload: u64, @@ -31,6 +33,7 @@ impl PeerStatsUpdate { let collected_at = Utc::now().naive_utc(); Self { location_id, + gateway_id, device_id, collected_at, endpoint, diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index d279f47f85..ae2f6e86da 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -667,6 +667,7 @@ impl GatewayHandler { match try_protos_into_stats_message( peer_stats.clone(), self.gateway.network_id, + self.gateway.id, device_id, ) { None => { diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index e35d265660..a482a2519d 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -95,6 +95,7 @@ pub fn send_multiple_wireguard_events(events: Vec, wg_tx: &Sender< fn try_protos_into_stats_message( proto_stats: PeerStats, location_id: Id, + gateway_id: Id, device_id: Id, ) -> Option { // try to parse endpoint @@ -106,6 +107,7 @@ fn try_protos_into_stats_message( Some(PeerStatsUpdate::new( location_id, + gateway_id, device_id, endpoint, proto_stats.upload, diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index 8e03c0200b..c7039ced5d 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -96,6 +96,7 @@ impl SessionState { let vpn_session_stats = VpnSessionStats::new( self.session_id, + peer_stats_update.gateway_id, peer_stats_update.collected_at, peer_stats_update.latest_handshake, peer_stats_update.endpoint.to_string(), diff --git a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql index f65befa775..eb300898ff 100644 --- a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql +++ b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql @@ -16,7 +16,7 @@ CREATE TABLE vpn_client_session ( state vpn_client_session_state NOT NULL DEFAULT 'new', FOREIGN KEY (location_id) REFERENCES wireguard_network(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES "user"(id) ON DELETE CASCADE, - FOREIGN KEY (device_id) REFERENCES device(id) ON DELETE SET NULL + FOREIGN KEY (device_id) REFERENCES device(id) ON DELETE CASCADE ); CREATE TABLE vpn_session_stats ( From 795bc670210e4719a0e8b7875f66a6448ccef5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 16 Jan 2026 09:15:16 +0100 Subject: [PATCH 38/52] update query data --- ...c669bc336d98b8b97c150da974ffcddb0006.json} | 5 ++-- ...5fb2749f754ee640c8dbe3227169a4e341406.json | 24 ------------------- ...63f2193d7055fcb1da31c8a08e9130584780.json} | 5 ++-- ...51cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json | 2 +- ...b30d6bfc9f07969d21d4044f5bab696e82f3.json} | 23 +++++++++++------- ...109f00658195b025e08e22962fb9c247d89d6.json | 2 +- ...de853deb5d6ce935931369e29d0dbef8dabb4.json | 2 +- ...ed7f93940c3b9fe3d19dd759e367fea9d776.json} | 23 +++++++++++------- ...0d00da99aed2e154bf071f37e29e069489a6.json} | 22 ++++++++++------- ...849660ab5ae3c9e7057dd5c24877c3fdcedbe.json | 2 +- ...c5b4da1599de659cd8315e331d6caf203fa4.json} | 22 ++++++++++------- ...1cbe7b8cb6f59a776ca3d6312270584561b5.json} | 8 +++---- 12 files changed, 71 insertions(+), 69 deletions(-) rename .sqlx/{query-e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56.json => query-04595bcd734988ca8b16e411ab0fc669bc336d98b8b97c150da974ffcddb0006.json} (55%) delete mode 100644 .sqlx/query-0a6eedbe05b3b456c68e40403565fb2749f754ee640c8dbe3227169a4e341406.json rename .sqlx/{query-c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7.json => query-4817eadac959d70ef9450665c99363f2193d7055fcb1da31c8a08e9130584780.json} (61%) rename .sqlx/{query-ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283.json => query-51ea0f8bdb0e0e923ba2b6ae619fb30d6bfc9f07969d21d4044f5bab696e82f3.json} (69%) rename .sqlx/{query-e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c.json => query-71a4511761ace8b924ec71cfee1eed7f93940c3b9fe3d19dd759e367fea9d776.json} (74%) rename .sqlx/{query-21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603.json => query-904480c73c4b79df1cafe0a9bb070d00da99aed2e154bf071f37e29e069489a6.json} (74%) rename .sqlx/{query-1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af.json => query-c154aea1df6c3f273a1e7ab9c9a1c5b4da1599de659cd8315e331d6caf203fa4.json} (73%) rename .sqlx/{query-f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4.json => query-c1b46390542c269b444beee161fb1cbe7b8cb6f59a776ca3d6312270584561b5.json} (53%) diff --git a/.sqlx/query-e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56.json b/.sqlx/query-04595bcd734988ca8b16e411ab0fc669bc336d98b8b97c150da974ffcddb0006.json similarity index 55% rename from .sqlx/query-e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56.json rename to .sqlx/query-04595bcd734988ca8b16e411ab0fc669bc336d98b8b97c150da974ffcddb0006.json index c4d19afc5a..0bf7217880 100644 --- a/.sqlx/query-e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56.json +++ b/.sqlx/query-04595bcd734988ca8b16e411ab0fc669bc336d98b8b97c150da974ffcddb0006.json @@ -1,10 +1,11 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"vpn_session_stats\" SET \"session_id\" = $2,\"collected_at\" = $3,\"latest_handshake\" = $4,\"endpoint\" = $5,\"total_upload\" = $6,\"total_download\" = $7,\"upload_diff\" = $8,\"download_diff\" = $9 WHERE id = $1", + "query": "UPDATE \"vpn_session_stats\" SET \"session_id\" = $2,\"gateway_id\" = $3,\"collected_at\" = $4,\"latest_handshake\" = $5,\"endpoint\" = $6,\"total_upload\" = $7,\"total_download\" = $8,\"upload_diff\" = $9,\"download_diff\" = $10 WHERE id = $1", "describe": { "columns": [], "parameters": { "Left": [ + "Int8", "Int8", "Int8", "Timestamp", @@ -18,5 +19,5 @@ }, "nullable": [] }, - "hash": "e1010c3b7a3ea7dc8c418d22b42f50e2abdc74addccce5b36ca53cd7f057fd56" + "hash": "04595bcd734988ca8b16e411ab0fc669bc336d98b8b97c150da974ffcddb0006" } diff --git a/.sqlx/query-0a6eedbe05b3b456c68e40403565fb2749f754ee640c8dbe3227169a4e341406.json b/.sqlx/query-0a6eedbe05b3b456c68e40403565fb2749f754ee640c8dbe3227169a4e341406.json deleted file mode 100644 index 26e1f5ec7e..0000000000 --- a/.sqlx/query-0a6eedbe05b3b456c68e40403565fb2749f754ee640c8dbe3227169a4e341406.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "WITH stats AS (SELECT * FROM wireguard_peer_stats_view WHERE device_id = $1 AND network = $2) SELECT COALESCE( ( SELECT latest_handshake \"latest_handshake: NaiveDateTime\" FROM stats WHERE latest_handshake_diff > $3 ORDER BY collected_at DESC LIMIT 1 ), ( SELECT latest_handshake \"latest_handshake: NaiveDateTime\" FROM stats ORDER BY collected_at LIMIT 1 ) ) AS latest_handshake", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "latest_handshake", - "type_info": "Timestamp" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Interval" - ] - }, - "nullable": [ - null - ] - }, - "hash": "0a6eedbe05b3b456c68e40403565fb2749f754ee640c8dbe3227169a4e341406" -} diff --git a/.sqlx/query-c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7.json b/.sqlx/query-4817eadac959d70ef9450665c99363f2193d7055fcb1da31c8a08e9130584780.json similarity index 61% rename from .sqlx/query-c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7.json rename to .sqlx/query-4817eadac959d70ef9450665c99363f2193d7055fcb1da31c8a08e9130584780.json index ab1863bd71..057bf00a3b 100644 --- a/.sqlx/query-c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7.json +++ b/.sqlx/query-4817eadac959d70ef9450665c99363f2193d7055fcb1da31c8a08e9130584780.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"vpn_session_stats\" (\"session_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", + "query": "INSERT INTO \"vpn_session_stats\" (\"session_id\",\"gateway_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id", "describe": { "columns": [ { @@ -11,6 +11,7 @@ ], "parameters": { "Left": [ + "Int8", "Int8", "Timestamp", "Timestamp", @@ -25,5 +26,5 @@ false ] }, - "hash": "c22a3d851ab7c4c22114984f146d98d798b879bd0c14ad1f467b51cb53391db7" + "hash": "4817eadac959d70ef9450665c99363f2193d7055fcb1da31c8a08e9130584780" } diff --git a/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json b/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json index a6b7713486..439064d20e 100644 --- a/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json +++ b/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json @@ -70,7 +70,7 @@ false, false, false, - true, + false, false, false, false, diff --git a/.sqlx/query-ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283.json b/.sqlx/query-51ea0f8bdb0e0e923ba2b6ae619fb30d6bfc9f07969d21d4044f5bab696e82f3.json similarity index 69% rename from .sqlx/query-ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283.json rename to .sqlx/query-51ea0f8bdb0e0e923ba2b6ae619fb30d6bfc9f07969d21d4044f5bab696e82f3.json index 340dcda4cf..6bd9d7c7fa 100644 --- a/.sqlx/query-ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283.json +++ b/.sqlx/query-51ea0f8bdb0e0e923ba2b6ae619fb30d6bfc9f07969d21d4044f5bab696e82f3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, session_id, collected_at, latest_handshake, endpoint, total_upload, total_download, upload_diff, download_diff\n \tFROM vpn_session_stats WHERE session_id = $1 ORDER BY collected_at DESC LIMIT 1", + "query": "SELECT st.id, session_id, gateway_id, collected_at, latest_handshake, endpoint, total_upload, total_download, upload_diff, download_diff FROM vpn_session_stats st JOIN vpn_client_session se ON session_id = se.id WHERE device_id = $1 AND location_id = $2 ORDER BY collected_at DESC LIMIT 1", "describe": { "columns": [ { @@ -15,42 +15,48 @@ }, { "ordinal": 2, + "name": "gateway_id", + "type_info": "Int8" + }, + { + "ordinal": 3, "name": "collected_at", "type_info": "Timestamp" }, { - "ordinal": 3, + "ordinal": 4, "name": "latest_handshake", "type_info": "Timestamp" }, { - "ordinal": 4, + "ordinal": 5, "name": "endpoint", "type_info": "Text" }, { - "ordinal": 5, + "ordinal": 6, "name": "total_upload", "type_info": "Int8" }, { - "ordinal": 6, + "ordinal": 7, "name": "total_download", "type_info": "Int8" }, { - "ordinal": 7, + "ordinal": 8, "name": "upload_diff", "type_info": "Int8" }, { - "ordinal": 8, + "ordinal": 9, "name": "download_diff", "type_info": "Int8" } ], "parameters": { "Left": [ + "Int8", "Int8" ] }, @@ -63,8 +69,9 @@ false, false, false, + false, false ] }, - "hash": "ab6b0e27cf3ccdd6ca490eb6c01fdeada1a1cd59bf756867fd4777e13d768283" + "hash": "51ea0f8bdb0e0e923ba2b6ae619fb30d6bfc9f07969d21d4044f5bab696e82f3" } diff --git a/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json b/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json index 01e95086f0..3f81c8f0d4 100644 --- a/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json +++ b/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json @@ -69,7 +69,7 @@ false, false, false, - true, + false, false, false, false, diff --git a/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json b/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json index 1daf28db36..16c69de7a2 100644 --- a/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json +++ b/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json @@ -67,7 +67,7 @@ false, false, false, - true, + false, false, false, false, diff --git a/.sqlx/query-e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c.json b/.sqlx/query-71a4511761ace8b924ec71cfee1eed7f93940c3b9fe3d19dd759e367fea9d776.json similarity index 74% rename from .sqlx/query-e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c.json rename to .sqlx/query-71a4511761ace8b924ec71cfee1eed7f93940c3b9fe3d19dd759e367fea9d776.json index 875ff12d94..70d7a9cb7f 100644 --- a/.sqlx/query-e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c.json +++ b/.sqlx/query-71a4511761ace8b924ec71cfee1eed7f93940c3b9fe3d19dd759e367fea9d776.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT st.id, session_id, collected_at, latest_handshake, endpoint, total_upload, total_download, upload_diff, download_diff FROM vpn_session_stats st JOIN vpn_client_session se ON session_id = se.id WHERE device_id = $1 AND location_id = $2 ORDER BY collected_at DESC LIMIT 1", + "query": "SELECT id, \"session_id\",\"gateway_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\" FROM \"vpn_session_stats\" WHERE id = $1", "describe": { "columns": [ { @@ -15,43 +15,47 @@ }, { "ordinal": 2, + "name": "gateway_id", + "type_info": "Int8" + }, + { + "ordinal": 3, "name": "collected_at", "type_info": "Timestamp" }, { - "ordinal": 3, + "ordinal": 4, "name": "latest_handshake", "type_info": "Timestamp" }, { - "ordinal": 4, + "ordinal": 5, "name": "endpoint", "type_info": "Text" }, { - "ordinal": 5, + "ordinal": 6, "name": "total_upload", "type_info": "Int8" }, { - "ordinal": 6, + "ordinal": 7, "name": "total_download", "type_info": "Int8" }, { - "ordinal": 7, + "ordinal": 8, "name": "upload_diff", "type_info": "Int8" }, { - "ordinal": 8, + "ordinal": 9, "name": "download_diff", "type_info": "Int8" } ], "parameters": { "Left": [ - "Int8", "Int8" ] }, @@ -64,8 +68,9 @@ false, false, false, + false, false ] }, - "hash": "e8a00314f4cffdbffcd26ce8cc166771f7759689c60d182aaa7feb051856910c" + "hash": "71a4511761ace8b924ec71cfee1eed7f93940c3b9fe3d19dd759e367fea9d776" } diff --git a/.sqlx/query-21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603.json b/.sqlx/query-904480c73c4b79df1cafe0a9bb070d00da99aed2e154bf071f37e29e069489a6.json similarity index 74% rename from .sqlx/query-21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603.json rename to .sqlx/query-904480c73c4b79df1cafe0a9bb070d00da99aed2e154bf071f37e29e069489a6.json index aefe121174..207905f29e 100644 --- a/.sqlx/query-21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603.json +++ b/.sqlx/query-904480c73c4b79df1cafe0a9bb070d00da99aed2e154bf071f37e29e069489a6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"session_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\" FROM \"vpn_session_stats\"", + "query": "SELECT id, \"session_id\",\"gateway_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\" FROM \"vpn_session_stats\"", "describe": { "columns": [ { @@ -15,36 +15,41 @@ }, { "ordinal": 2, + "name": "gateway_id", + "type_info": "Int8" + }, + { + "ordinal": 3, "name": "collected_at", "type_info": "Timestamp" }, { - "ordinal": 3, + "ordinal": 4, "name": "latest_handshake", "type_info": "Timestamp" }, { - "ordinal": 4, + "ordinal": 5, "name": "endpoint", "type_info": "Text" }, { - "ordinal": 5, + "ordinal": 6, "name": "total_upload", "type_info": "Int8" }, { - "ordinal": 6, + "ordinal": 7, "name": "total_download", "type_info": "Int8" }, { - "ordinal": 7, + "ordinal": 8, "name": "upload_diff", "type_info": "Int8" }, { - "ordinal": 8, + "ordinal": 9, "name": "download_diff", "type_info": "Int8" } @@ -61,8 +66,9 @@ false, false, false, + false, false ] }, - "hash": "21ee4396cef32d74121dc43235e1785d0aca658a64256c28d4b044e77f6fb603" + "hash": "904480c73c4b79df1cafe0a9bb070d00da99aed2e154bf071f37e29e069489a6" } diff --git a/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json b/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json index 74a4381829..9b3ff8deec 100644 --- a/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json +++ b/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json @@ -69,7 +69,7 @@ false, false, false, - true, + false, false, false, false, diff --git a/.sqlx/query-1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af.json b/.sqlx/query-c154aea1df6c3f273a1e7ab9c9a1c5b4da1599de659cd8315e331d6caf203fa4.json similarity index 73% rename from .sqlx/query-1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af.json rename to .sqlx/query-c154aea1df6c3f273a1e7ab9c9a1c5b4da1599de659cd8315e331d6caf203fa4.json index d132f8e9f3..e5b2104a6a 100644 --- a/.sqlx/query-1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af.json +++ b/.sqlx/query-c154aea1df6c3f273a1e7ab9c9a1c5b4da1599de659cd8315e331d6caf203fa4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"session_id\",\"collected_at\",\"latest_handshake\",\"endpoint\",\"total_upload\",\"total_download\",\"upload_diff\",\"download_diff\" FROM \"vpn_session_stats\" WHERE id = $1", + "query": "SELECT id, session_id, gateway_id, collected_at, latest_handshake, endpoint, total_upload, total_download, upload_diff, download_diff\n \tFROM vpn_session_stats WHERE session_id = $1 ORDER BY collected_at DESC LIMIT 1", "describe": { "columns": [ { @@ -15,36 +15,41 @@ }, { "ordinal": 2, + "name": "gateway_id", + "type_info": "Int8" + }, + { + "ordinal": 3, "name": "collected_at", "type_info": "Timestamp" }, { - "ordinal": 3, + "ordinal": 4, "name": "latest_handshake", "type_info": "Timestamp" }, { - "ordinal": 4, + "ordinal": 5, "name": "endpoint", "type_info": "Text" }, { - "ordinal": 5, + "ordinal": 6, "name": "total_upload", "type_info": "Int8" }, { - "ordinal": 6, + "ordinal": 7, "name": "total_download", "type_info": "Int8" }, { - "ordinal": 7, + "ordinal": 8, "name": "upload_diff", "type_info": "Int8" }, { - "ordinal": 8, + "ordinal": 9, "name": "download_diff", "type_info": "Int8" } @@ -63,8 +68,9 @@ false, false, false, + false, false ] }, - "hash": "1c2a2a54e5f9b28521d97e1496f47840e602bd006a7aec6790376325158449af" + "hash": "c154aea1df6c3f273a1e7ab9c9a1c5b4da1599de659cd8315e331d6caf203fa4" } diff --git a/.sqlx/query-f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4.json b/.sqlx/query-c1b46390542c269b444beee161fb1cbe7b8cb6f59a776ca3d6312270584561b5.json similarity index 53% rename from .sqlx/query-f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4.json rename to .sqlx/query-c1b46390542c269b444beee161fb1cbe7b8cb6f59a776ca3d6312270584561b5.json index 481cb9a635..9dd83d5713 100644 --- a/.sqlx/query-f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4.json +++ b/.sqlx/query-c1b46390542c269b444beee161fb1cbe7b8cb6f59a776ca3d6312270584561b5.json @@ -1,11 +1,11 @@ { "db_name": "PostgreSQL", - "query": "SELECT s.device_id \"device_id!\", date_trunc($1, collected_at) \"collected_at!: NaiveDateTime\", CAST(sum(download_diff) AS bigint) \"download!\", CAST(sum(upload_diff) AS bigint) \"upload!\" FROM vpn_session_stats INNER JOIN vpn_client_session s ON session_id = s.id WHERE s.device_id = ANY($2) AND collected_at >= $3 AND s.location_id = $4 GROUP BY device_id, collected_at ORDER BY device_id, collected_at", + "query": "SELECT s.device_id, date_trunc($1, collected_at) \"collected_at!: NaiveDateTime\", CAST(sum(download_diff) AS bigint) \"download!\", CAST(sum(upload_diff) AS bigint) \"upload!\" FROM vpn_session_stats INNER JOIN vpn_client_session s ON session_id = s.id WHERE s.device_id = ANY($2) AND collected_at >= $3 AND s.location_id = $4 GROUP BY device_id, collected_at ORDER BY device_id, collected_at", "describe": { "columns": [ { "ordinal": 0, - "name": "device_id!", + "name": "device_id", "type_info": "Int8" }, { @@ -33,11 +33,11 @@ ] }, "nullable": [ - true, + false, null, null, null ] }, - "hash": "f529ce3a7e0177ffa74551f6f66d0633419d573e63edceb419e972c8dba451f4" + "hash": "c1b46390542c269b444beee161fb1cbe7b8cb6f59a776ca3d6312270584561b5" } From 475d6874daff0271e7ea6e6b65c0dffeba883217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 16 Jan 2026 09:18:43 +0100 Subject: [PATCH 39/52] cleanup --- crates/defguard_common/src/db/models/vpn_session_stats.rs | 1 + crates/defguard_common/src/db/models/wireguard.rs | 7 +++---- crates/defguard_core/tests/integration/api/mod.rs | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_session_stats.rs b/crates/defguard_common/src/db/models/vpn_session_stats.rs index 67b732446d..809fd17d7c 100644 --- a/crates/defguard_common/src/db/models/vpn_session_stats.rs +++ b/crates/defguard_common/src/db/models/vpn_session_stats.rs @@ -25,6 +25,7 @@ pub struct VpnSessionStats { } impl VpnSessionStats { + #![allow(clippy::too_many_arguments)] pub fn new( session_id: Id, gateway_id: Id, diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 91be8c9930..04275b90ad 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -1179,14 +1179,13 @@ pub async fn networks_stats( mod test { use std::str::FromStr; - use crate::db::{models::wireguard_peer_stats::WireguardPeerStats, setup_pool}; - use chrono::{SubsecRound, TimeDelta, Utc}; + use crate::db::setup_pool; use matches::assert_matches; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - // FIXME: test new connection logic + // FIXME(mwojcik): rewrite for new stats implementation // #[sqlx::test] // async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { // let pool = setup_pool(options).await; @@ -1251,7 +1250,7 @@ mod test { // ); // } - // FIXME: test new connection logic + // FIXME(mwojcik): rewrite for new stats implementation // #[sqlx::test] // async fn test_connected_at_always_connected(_: PgPoolOptions, options: PgConnectOptions) { // let pool = setup_pool(options).await; diff --git a/crates/defguard_core/tests/integration/api/mod.rs b/crates/defguard_core/tests/integration/api/mod.rs index 218ff73cf8..8783bf4a87 100644 --- a/crates/defguard_core/tests/integration/api/mod.rs +++ b/crates/defguard_core/tests/integration/api/mod.rs @@ -17,7 +17,8 @@ mod wireguard; mod wireguard_network_allowed_groups; mod wireguard_network_devices; mod wireguard_network_import; -mod wireguard_network_stats; +// FIXME(mwojcik): rewrite for new stats implementation +// mod wireguard_network_stats; mod worker; const TEST_SERVER_URL: &str = "http://localhost:3000/"; From f2ed7820beceffc19dd585dda1928cc1801b413d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 16 Jan 2026 09:27:30 +0100 Subject: [PATCH 40/52] disable disconnect for now --- crates/defguard_session_manager/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index e677002fff..359f978088 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -137,6 +137,8 @@ impl SessionManager { } async fn update_inactive_session_status(&self) -> Result<(), SessionManagerError> { - unimplemented!() + // TODO(mwojcik): actually implement this logic + debug!("Disconnecting inactive VPN sessions"); + Ok(()) } } From 45a19a3cd94bb8fa98628dbd99c6b611beb9ac7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 19 Jan 2026 08:48:06 +0100 Subject: [PATCH 41/52] fix nullable timestamp fields --- ...2098903b2ab7b1ded07501f2d7ce822c8d712.json | 23 ------------------- ...51cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json | 4 ++-- ...109f00658195b025e08e22962fb9c247d89d6.json | 4 ++-- ...de853deb5d6ce935931369e29d0dbef8dabb4.json | 4 ++-- ...d484c36ae068f99ca8219cabf80493cc887f6.json | 23 +++++++++++++++++++ ...849660ab5ae3c9e7057dd5c24877c3fdcedbe.json | 4 ++-- .../defguard_common/src/db/models/device.rs | 2 +- ...5121935_[2.0.0]_vpn_client_sessions.up.sql | 4 ++-- 8 files changed, 34 insertions(+), 34 deletions(-) delete mode 100644 .sqlx/query-28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712.json create mode 100644 .sqlx/query-5fbbd7ce67f3e5baba8af166b39d484c36ae068f99ca8219cabf80493cc887f6.json diff --git a/.sqlx/query-28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712.json b/.sqlx/query-28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712.json deleted file mode 100644 index 277f3af122..0000000000 --- a/.sqlx/query-28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT connected_at FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND connected_at IS NOT NULL ORDER BY connected_at DESC LIMIT 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "connected_at", - "type_info": "Timestamp" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "28bc2fb85b97df98dae701d45572098903b2ab7b1ded07501f2d7ce822c8d712" -} diff --git a/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json b/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json index 439064d20e..faff087142 100644 --- a/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json +++ b/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json @@ -72,8 +72,8 @@ false, false, false, - false, - false, + true, + true, false, false ] diff --git a/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json b/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json index 3f81c8f0d4..0739e3bc67 100644 --- a/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json +++ b/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json @@ -71,8 +71,8 @@ false, false, false, - false, - false, + true, + true, false, false ] diff --git a/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json b/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json index 16c69de7a2..f7bc62c7d5 100644 --- a/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json +++ b/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json @@ -69,8 +69,8 @@ false, false, false, - false, - false, + true, + true, false, false ] diff --git a/.sqlx/query-5fbbd7ce67f3e5baba8af166b39d484c36ae068f99ca8219cabf80493cc887f6.json b/.sqlx/query-5fbbd7ce67f3e5baba8af166b39d484c36ae068f99ca8219cabf80493cc887f6.json new file mode 100644 index 0000000000..8bbed163b7 --- /dev/null +++ b/.sqlx/query-5fbbd7ce67f3e5baba8af166b39d484c36ae068f99ca8219cabf80493cc887f6.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT connected_at \"connected_at!\" FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND connected_at IS NOT NULL ORDER BY connected_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "connected_at!", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + true + ] + }, + "hash": "5fbbd7ce67f3e5baba8af166b39d484c36ae068f99ca8219cabf80493cc887f6" +} diff --git a/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json b/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json index 9b3ff8deec..0894c51b45 100644 --- a/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json +++ b/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json @@ -71,8 +71,8 @@ false, false, false, - false, - false, + true, + true, false, false ] diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index a365a75d2b..ed8cffb478 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -1004,7 +1004,7 @@ impl Device { location_id: Id, ) -> Result, SqlxError> { query_scalar!( - "SELECT connected_at FROM vpn_client_session \ + "SELECT connected_at \"connected_at!\" FROM vpn_client_session \ WHERE location_id = $1 AND device_id = $2 AND connected_at IS NOT NULL \ ORDER BY connected_at DESC LIMIT 1", location_id, diff --git a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql index eb300898ff..b6b8ab1585 100644 --- a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql +++ b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql @@ -10,8 +10,8 @@ CREATE TABLE vpn_client_session ( user_id bigint NOT NULL, device_id bigint NOT NULL, created_at timestamp without time zone NOT NULL DEFAULT current_timestamp, - connected_at timestamp without time zone NOT NULL, - disconnected_at timestamp without time zone NOT NULL, + connected_at timestamp without time zone NULL, + disconnected_at timestamp without time zone NULL, mfa boolean NOT NULL, state vpn_client_session_state NOT NULL DEFAULT 'new', FOREIGN KEY (location_id) REFERENCES wireguard_network(id) ON DELETE CASCADE, From 80ea30c32206d5d1d460120957377e542978b51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 19 Jan 2026 11:29:45 +0100 Subject: [PATCH 42/52] fetch inactive sessions --- .../src/db/models/vpn_client_session.rs | 46 ++++++++++++++++- crates/defguard_session_manager/src/lib.rs | 51 +++++++++++++++++-- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index 883f70b761..c34b0638d3 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -2,7 +2,10 @@ use chrono::{NaiveDateTime, Utc}; use model_derive::Model; use sqlx::{Error as SqlxError, Type, query_as}; -use crate::db::{Id, NoId, models::vpn_session_stats::VpnSessionStats}; +use crate::db::{ + Id, NoId, + models::{WireguardNetwork, vpn_session_stats::VpnSessionStats}, +}; #[derive(Default, Type)] #[sqlx(type_name = "vpn_client_session_state", rename_all = "lowercase")] @@ -96,4 +99,45 @@ impl VpnClientSession { .fetch_optional(executor) .await } + + /// Fetch active sessions which have become inactive for a specific location + pub async fn get_inactive<'e, E: sqlx::PgExecutor<'e>>( + executor: E, + location: &WireguardNetwork, + ) -> Result, SqlxError> { + query_as!( + Self, + "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, \ + mfa, state \"state: VpnClientSessionState\" \ + FROM vpn_client_session s \ + LEFT JOIN LATERAL ( \ + SELECT latest_handshake \ + FROM vpn_session_stats \ + WHERE session_id = s.id \ + ORDER BY collected_at DESC \ + LIMIT 1 \ + ) ss ON true \ + WHERE location_id = $1 AND state = 'connected' \ + AND (NOW() - ss.latest_handshake) > $2 * interval '1 second'", + location.id, + f64::from(location.peer_disconnect_threshold) + ).fetch_all(executor).await + } + + /// Fetch sessions that were created but have not become `connected` within the disconnect threshold + pub async fn get_never_connected<'e, E: sqlx::PgExecutor<'e>>( + executor: E, + location: &WireguardNetwork, + ) -> Result, SqlxError> { + query_as!( + Self, + "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ + mfa, state \"state: VpnClientSessionState\" \ + FROM vpn_client_session \ + WHERE location_id = $1 AND state = 'new' \ + AND (NOW() - created_at) > $2 * interval '1 second'", + location.id, + f64::from(location.peer_disconnect_threshold) + ).fetch_all(executor).await + } } diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 359f978088..84447b7888 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -1,4 +1,7 @@ -use defguard_common::messages::peer_stats_update::PeerStatsUpdate; +use defguard_common::{ + db::models::{WireguardNetwork, vpn_client_session::VpnClientSession}, + messages::peer_stats_update::PeerStatsUpdate, +}; use sqlx::{PgConnection, PgPool}; use tokio::{ sync::mpsc::UnboundedReceiver, @@ -136,9 +139,51 @@ impl SessionManager { Ok(()) } + /// Disconnect all inactive sessions + /// + /// A session is considered inactive once more than the configured `peer_disconnect_threshold` + /// has elapsed since the last registered handshake has ocurred. + /// This threshold is specified per location. async fn update_inactive_session_status(&self) -> Result<(), SessionManagerError> { - // TODO(mwojcik): actually implement this logic - debug!("Disconnecting inactive VPN sessions"); + info!("Disconnecting inactive VPN sessions"); + + // begin DB transaction + let mut transaction = self.pool.begin().await?; + + // get all locations + let locations = WireguardNetwork::all(&mut *transaction).await?; + let locations_count = locations.len(); + + for (index, location) in locations.into_iter().enumerate() { + debug!( + "[{index}/{locations_count}] Disconnecting inactive sessions in location {location}" + ); + + // get all connected sessions which have become inactive + let inactive_sessions = + VpnClientSession::get_inactive(&mut *transaction, &location).await?; + + debug!( + "Found {} inactive VPN sessions in location {location}", + inactive_sessions.len() + ); + + // get all sessions which were created but have never connected + // this is only relevant for MFA locations + let unused_sessions = + VpnClientSession::get_never_connected(&mut *transaction, &location).await?; + + debug!( + "Found {} new VPN sessions which have not connected within required time in location {location}", + unused_sessions.len() + ); + } + + // commit DB transaction after processing all inactive sessions + transaction.commit().await?; + + debug!("Finished processing inactive VPN sessions"); + Ok(()) } } From 61d66f7191be8ddd88aa61287a1322e72f1fdcc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 19 Jan 2026 11:40:24 +0100 Subject: [PATCH 43/52] handle marking sessions as disconnected --- crates/defguard_session_manager/src/lib.rs | 34 +++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 84447b7888..cd24fbb582 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -1,5 +1,9 @@ +use chrono::Utc; use defguard_common::{ - db::models::{WireguardNetwork, vpn_client_session::VpnClientSession}, + db::{ + Id, + models::{WireguardNetwork, vpn_client_session::VpnClientSession}, + }, messages::peer_stats_update::PeerStatsUpdate, }; use sqlx::{PgConnection, PgPool}; @@ -168,6 +172,14 @@ impl SessionManager { inactive_sessions.len() ); + for session in inactive_sessions { + debug!( + "Disconnecting inactive session for user {}, device {} in location {location}", + session.user_id, session.device_id + ); + Self::disconnect_session(&mut *&mut transaction, session).await?; + } + // get all sessions which were created but have never connected // this is only relevant for MFA locations let unused_sessions = @@ -177,6 +189,14 @@ impl SessionManager { "Found {} new VPN sessions which have not connected within required time in location {location}", unused_sessions.len() ); + + for session in unused_sessions { + debug!( + "Disconnecting never connected session for user {}, device {} in location {location}", + session.user_id, session.device_id + ); + Self::disconnect_session(&mut *&mut transaction, session).await?; + } } // commit DB transaction after processing all inactive sessions @@ -186,4 +206,16 @@ impl SessionManager { Ok(()) } + + /// Helper user to mark session as disconnected and trigger necessary sideffects + async fn disconnect_session( + transaction: &mut PgConnection, + mut session: VpnClientSession, + ) -> Result<(), SessionManagerError> { + session.disconnected_at = Some(Utc::now().naive_utc()); + session.state = + defguard_common::db::models::vpn_client_session::VpnClientSessionState::Disconnected; + session.save(&mut *transaction).await?; + Ok(()) + } } From eecad793037e60ee48b92d0c9d8a3d08b04bcd14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 19 Jan 2026 11:53:39 +0100 Subject: [PATCH 44/52] store type of MFA used for connection --- .../src/db/models/vpn_client_session.rs | 16 +++++++++------- .../defguard_common/src/db/models/wireguard.rs | 2 +- crates/defguard_session_manager/src/lib.rs | 4 ++-- .../src/session_state.rs | 6 +++--- ...0115121935_[2.0.0]_vpn_client_sessions.up.sql | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index c34b0638d3..8f2167e2ef 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -4,7 +4,7 @@ use sqlx::{Error as SqlxError, Type, query_as}; use crate::db::{ Id, NoId, - models::{WireguardNetwork, vpn_session_stats::VpnSessionStats}, + models::{WireguardNetwork, vpn_session_stats::VpnSessionStats, wireguard::LocationMfaMode}, }; #[derive(Default, Type)] @@ -27,7 +27,9 @@ pub struct VpnClientSession { pub created_at: NaiveDateTime, pub connected_at: Option, pub disconnected_at: Option, - pub mfa: bool, + // TODO: use actual MFA method used to connect + #[model(enum)] + pub mfa_mode: LocationMfaMode, #[model(enum)] pub state: VpnClientSessionState, } @@ -38,7 +40,7 @@ impl VpnClientSession { user_id: Id, device_id: Id, connected_at: Option, - mfa: bool, + mfa_mode: LocationMfaMode, ) -> Self { // determine session state let state = if connected_at.is_some() { @@ -55,7 +57,7 @@ impl VpnClientSession { created_at: Utc::now().naive_utc(), connected_at, disconnected_at: None, - mfa, + mfa_mode, state, } } @@ -73,7 +75,7 @@ impl VpnClientSession { query_as!( Self, "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ - mfa, state \"state: VpnClientSessionState\" \ + mfa_mode \"mfa_mode: LocationMfaMode\", state \"state: VpnClientSessionState\" \ FROM vpn_client_session \ WHERE location_id = $1 AND device_id = $2", location_id, @@ -108,7 +110,7 @@ impl VpnClientSession { query_as!( Self, "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, \ - mfa, state \"state: VpnClientSessionState\" \ + mfa_mode \"mfa_mode: LocationMfaMode\", state \"state: VpnClientSessionState\" \ FROM vpn_client_session s \ LEFT JOIN LATERAL ( \ SELECT latest_handshake \ @@ -132,7 +134,7 @@ impl VpnClientSession { query_as!( Self, "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ - mfa, state \"state: VpnClientSessionState\" \ + mfa_mode \"mfa_mode: LocationMfaMode\", state \"state: VpnClientSessionState\" \ FROM vpn_client_session \ WHERE location_id = $1 AND state = 'new' \ AND (NOW() - created_at) > $2 * interval '1 second'", diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index 04275b90ad..0355c4d407 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -1018,7 +1018,7 @@ impl WireguardNetwork { query_as!( VpnClientSession, "SELECT id, location_id, user_id, device_id, \ - created_at, connected_at, disconnected_at, mfa, state \"state: VpnClientSessionState\" \ + created_at, connected_at, disconnected_at, mfa_mode \"mfa_mode: LocationMfaMode\", state \"state: VpnClientSessionState\" \ FROM vpn_client_session \ WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", self.id, diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index cd24fbb582..93ee0ce504 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -177,7 +177,7 @@ impl SessionManager { "Disconnecting inactive session for user {}, device {} in location {location}", session.user_id, session.device_id ); - Self::disconnect_session(&mut *&mut transaction, session).await?; + Self::disconnect_session(&mut transaction, session).await?; } // get all sessions which were created but have never connected @@ -195,7 +195,7 @@ impl SessionManager { "Disconnecting never connected session for user {}, device {} in location {location}", session.user_id, session.device_id ); - Self::disconnect_session(&mut *&mut transaction, session).await?; + Self::disconnect_session(&mut transaction, session).await?; } } diff --git a/crates/defguard_session_manager/src/session_state.rs b/crates/defguard_session_manager/src/session_state.rs index c7039ced5d..66981e08ff 100644 --- a/crates/defguard_session_manager/src/session_state.rs +++ b/crates/defguard_session_manager/src/session_state.rs @@ -218,7 +218,7 @@ impl ActiveSessionsMap { // fetch location let location_id = stats_update.location_id; // wrap in block to avoid multiple mutable borrows - let (location_name, mfa_enabled) = { + let (location_name, mfa_mode) = { let location = self.get_location(&mut *transaction, location_id).await?; // check if a given peer is considered active and should be added to active sessions if Utc::now().naive_utc() - stats_update.latest_handshake @@ -230,7 +230,7 @@ impl ActiveSessionsMap { return Ok(None); }; - (location.name.clone(), location.mfa_enabled()) + (location.name.clone(), location.location_mfa_mode.clone()) }; // fetch other related objects from DB @@ -247,7 +247,7 @@ impl ActiveSessionsMap { user.id, device_id, Some(stats_update.latest_handshake), - mfa_enabled, + mfa_mode, ) .save(transaction) .await?; diff --git a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql index b6b8ab1585..98b96535f3 100644 --- a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql +++ b/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql @@ -12,7 +12,7 @@ CREATE TABLE vpn_client_session ( created_at timestamp without time zone NOT NULL DEFAULT current_timestamp, connected_at timestamp without time zone NULL, disconnected_at timestamp without time zone NULL, - mfa boolean NOT NULL, + mfa_mode location_mfa_mode NOT NULL, state vpn_client_session_state NOT NULL DEFAULT 'new', FOREIGN KEY (location_id) REFERENCES wireguard_network(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES "user"(id) ON DELETE CASCADE, From f363edded128efd90855c5840e659a75de232c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 19 Jan 2026 11:53:59 +0100 Subject: [PATCH 45/52] update query data --- ...b0917f7345c9d3a054b73e1176758a4ba1a9.json} | 19 +++- ...8dde89c59df69ede331ea25d3dbac93bc5b8.json} | 19 +++- ...8bff70bed69bec930e7331c576073f612677.json} | 17 +++- ...ddc6c1c82aad42f16dbda45003f13a1f6e33.json} | 19 +++- ...e26c24f3778a932d744b1378abbc0e04fb8a5.json | 93 +++++++++++++++++++ ...a7d392cee6a69bd7cbf6479cc2cf5294319c.json} | 17 +++- ...8efd92525a9773fe79c17f3a19081fd932f9.json} | 19 +++- ...11fa4ea8d63de6e7bd7a0f5cf41119cb3bf86.json | 93 +++++++++++++++++++ 8 files changed, 274 insertions(+), 22 deletions(-) rename .sqlx/{query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json => query-126b613d8b07d65836a20429bef3b0917f7345c9d3a054b73e1176758a4ba1a9.json} (73%) rename .sqlx/{query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json => query-1815955c24b6178c653bd7a0e4a18dde89c59df69ede331ea25d3dbac93bc5b8.json} (70%) rename .sqlx/{query-e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4.json => query-2ec4ae04a8cf90d7a062ce0b2c318bff70bed69bec930e7331c576073f612677.json} (64%) rename .sqlx/{query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json => query-83722331508d9f6347db04c44546ddc6c1c82aad42f16dbda45003f13a1f6e33.json} (71%) create mode 100644 .sqlx/query-b2894d8c60b044744f946ee6b9ae26c24f3778a932d744b1378abbc0e04fb8a5.json rename .sqlx/{query-2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7.json => query-c9808451bd3653635dfb455c6125a7d392cee6a69bd7cbf6479cc2cf5294319c.json} (60%) rename .sqlx/{query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json => query-dbb3290d7ec75771a626416e3cdb8efd92525a9773fe79c17f3a19081fd932f9.json} (72%) create mode 100644 .sqlx/query-e9bfbd2e39ddc1cc0f95258cbd711fa4ea8d63de6e7bd7a0f5cf41119cb3bf86.json diff --git a/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json b/.sqlx/query-126b613d8b07d65836a20429bef3b0917f7345c9d3a054b73e1176758a4ba1a9.json similarity index 73% rename from .sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json rename to .sqlx/query-126b613d8b07d65836a20429bef3b0917f7345c9d3a054b73e1176758a4ba1a9.json index f7bc62c7d5..045d193d63 100644 --- a/.sqlx/query-580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4.json +++ b/.sqlx/query-126b613d8b07d65836a20429bef3b0917f7345c9d3a054b73e1176758a4ba1a9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa\",\"state\" \"state: _\" FROM \"vpn_client_session\"", + "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_mode\" \"mfa_mode: _\",\"state\" \"state: _\" FROM \"vpn_client_session\"", "describe": { "columns": [ { @@ -40,8 +40,19 @@ }, { "ordinal": 7, - "name": "mfa", - "type_info": "Bool" + "name": "mfa_mode: _", + "type_info": { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + } }, { "ordinal": 8, @@ -75,5 +86,5 @@ false ] }, - "hash": "580f18cfdfb8624366fbfaf61d2de853deb5d6ce935931369e29d0dbef8dabb4" + "hash": "126b613d8b07d65836a20429bef3b0917f7345c9d3a054b73e1176758a4ba1a9" } diff --git a/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json b/.sqlx/query-1815955c24b6178c653bd7a0e4a18dde89c59df69ede331ea25d3dbac93bc5b8.json similarity index 70% rename from .sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json rename to .sqlx/query-1815955c24b6178c653bd7a0e4a18dde89c59df69ede331ea25d3dbac93bc5b8.json index 0739e3bc67..be4214451d 100644 --- a/.sqlx/query-5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6.json +++ b/.sqlx/query-1815955c24b6178c653bd7a0e4a18dde89c59df69ede331ea25d3dbac93bc5b8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa, state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_mode \"mfa_mode: LocationMfaMode\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", "describe": { "columns": [ { @@ -40,8 +40,19 @@ }, { "ordinal": 7, - "name": "mfa", - "type_info": "Bool" + "name": "mfa_mode: LocationMfaMode", + "type_info": { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + } }, { "ordinal": 8, @@ -77,5 +88,5 @@ false ] }, - "hash": "5237ea7bddc2b4e7b7accc42e5f109f00658195b025e08e22962fb9c247d89d6" + "hash": "1815955c24b6178c653bd7a0e4a18dde89c59df69ede331ea25d3dbac93bc5b8" } diff --git a/.sqlx/query-e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4.json b/.sqlx/query-2ec4ae04a8cf90d7a062ce0b2c318bff70bed69bec930e7331c576073f612677.json similarity index 64% rename from .sqlx/query-e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4.json rename to .sqlx/query-2ec4ae04a8cf90d7a062ce0b2c318bff70bed69bec930e7331c576073f612677.json index 3d81f088d8..47d0854851 100644 --- a/.sqlx/query-e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4.json +++ b/.sqlx/query-2ec4ae04a8cf90d7a062ce0b2c318bff70bed69bec930e7331c576073f612677.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"vpn_client_session\" (\"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa\",\"state\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", + "query": "INSERT INTO \"vpn_client_session\" (\"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_mode\",\"state\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", "describe": { "columns": [ { @@ -17,7 +17,18 @@ "Timestamp", "Timestamp", "Timestamp", - "Bool", + { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + }, { "Custom": { "name": "vpn_client_session_state", @@ -36,5 +47,5 @@ false ] }, - "hash": "e980a3f10a2968960f8e727a305e9fe417a3f7f734adc81052a707e15d17ccc4" + "hash": "2ec4ae04a8cf90d7a062ce0b2c318bff70bed69bec930e7331c576073f612677" } diff --git a/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json b/.sqlx/query-83722331508d9f6347db04c44546ddc6c1c82aad42f16dbda45003f13a1f6e33.json similarity index 71% rename from .sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json rename to .sqlx/query-83722331508d9f6347db04c44546ddc6c1c82aad42f16dbda45003f13a1f6e33.json index faff087142..08ecdf129a 100644 --- a/.sqlx/query-495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f.json +++ b/.sqlx/query-83722331508d9f6347db04c44546ddc6c1c82aad42f16dbda45003f13a1f6e33.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa, state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND device_id = $2", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_mode \"mfa_mode: LocationMfaMode\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND device_id = $2", "describe": { "columns": [ { @@ -40,8 +40,19 @@ }, { "ordinal": 7, - "name": "mfa", - "type_info": "Bool" + "name": "mfa_mode: LocationMfaMode", + "type_info": { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + } }, { "ordinal": 8, @@ -78,5 +89,5 @@ false ] }, - "hash": "495cf0ba3efac4da8a3a8daa79851cfa6f6df1f5b5f1e8b9f7ceb3850a85358f" + "hash": "83722331508d9f6347db04c44546ddc6c1c82aad42f16dbda45003f13a1f6e33" } diff --git a/.sqlx/query-b2894d8c60b044744f946ee6b9ae26c24f3778a932d744b1378abbc0e04fb8a5.json b/.sqlx/query-b2894d8c60b044744f946ee6b9ae26c24f3778a932d744b1378abbc0e04fb8a5.json new file mode 100644 index 0000000000..9250297586 --- /dev/null +++ b/.sqlx/query-b2894d8c60b044744f946ee6b9ae26c24f3778a932d744b1378abbc0e04fb8a5.json @@ -0,0 +1,93 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, mfa_mode \"mfa_mode: LocationMfaMode\", state \"state: VpnClientSessionState\" FROM vpn_client_session s LEFT JOIN LATERAL ( SELECT latest_handshake FROM vpn_session_stats WHERE session_id = s.id ORDER BY collected_at DESC LIMIT 1 ) ss ON true WHERE location_id = $1 AND state = 'connected' AND (NOW() - ss.latest_handshake) > $2 * interval '1 second'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "mfa_mode: LocationMfaMode", + "type_info": { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + } + }, + { + "ordinal": 8, + "name": "state: VpnClientSessionState", + "type_info": { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int8", + "Float8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + false + ] + }, + "hash": "b2894d8c60b044744f946ee6b9ae26c24f3778a932d744b1378abbc0e04fb8a5" +} diff --git a/.sqlx/query-2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7.json b/.sqlx/query-c9808451bd3653635dfb455c6125a7d392cee6a69bd7cbf6479cc2cf5294319c.json similarity index 60% rename from .sqlx/query-2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7.json rename to .sqlx/query-c9808451bd3653635dfb455c6125a7d392cee6a69bd7cbf6479cc2cf5294319c.json index 188ec6128f..004cbcf8c6 100644 --- a/.sqlx/query-2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7.json +++ b/.sqlx/query-c9808451bd3653635dfb455c6125a7d392cee6a69bd7cbf6479cc2cf5294319c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"vpn_client_session\" SET \"location_id\" = $2,\"user_id\" = $3,\"device_id\" = $4,\"created_at\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"mfa\" = $8,\"state\" = $9 WHERE id = $1", + "query": "UPDATE \"vpn_client_session\" SET \"location_id\" = $2,\"user_id\" = $3,\"device_id\" = $4,\"created_at\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"mfa_mode\" = $8,\"state\" = $9 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -12,7 +12,18 @@ "Timestamp", "Timestamp", "Timestamp", - "Bool", + { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + }, { "Custom": { "name": "vpn_client_session_state", @@ -29,5 +40,5 @@ }, "nullable": [] }, - "hash": "2ed217218bd187805b4f7c8011063493c7e18d05dfdc0a9e8f35b44a5d1141b7" + "hash": "c9808451bd3653635dfb455c6125a7d392cee6a69bd7cbf6479cc2cf5294319c" } diff --git a/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json b/.sqlx/query-dbb3290d7ec75771a626416e3cdb8efd92525a9773fe79c17f3a19081fd932f9.json similarity index 72% rename from .sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json rename to .sqlx/query-dbb3290d7ec75771a626416e3cdb8efd92525a9773fe79c17f3a19081fd932f9.json index 0894c51b45..f4d8d26d81 100644 --- a/.sqlx/query-aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe.json +++ b/.sqlx/query-dbb3290d7ec75771a626416e3cdb8efd92525a9773fe79c17f3a19081fd932f9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa\",\"state\" \"state: _\" FROM \"vpn_client_session\" WHERE id = $1", + "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_mode\" \"mfa_mode: _\",\"state\" \"state: _\" FROM \"vpn_client_session\" WHERE id = $1", "describe": { "columns": [ { @@ -40,8 +40,19 @@ }, { "ordinal": 7, - "name": "mfa", - "type_info": "Bool" + "name": "mfa_mode: _", + "type_info": { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + } }, { "ordinal": 8, @@ -77,5 +88,5 @@ false ] }, - "hash": "aa3234095ddddf88cda647a33a3849660ab5ae3c9e7057dd5c24877c3fdcedbe" + "hash": "dbb3290d7ec75771a626416e3cdb8efd92525a9773fe79c17f3a19081fd932f9" } diff --git a/.sqlx/query-e9bfbd2e39ddc1cc0f95258cbd711fa4ea8d63de6e7bd7a0f5cf41119cb3bf86.json b/.sqlx/query-e9bfbd2e39ddc1cc0f95258cbd711fa4ea8d63de6e7bd7a0f5cf41119cb3bf86.json new file mode 100644 index 0000000000..f5d922538b --- /dev/null +++ b/.sqlx/query-e9bfbd2e39ddc1cc0f95258cbd711fa4ea8d63de6e7bd7a0f5cf41119cb3bf86.json @@ -0,0 +1,93 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_mode \"mfa_mode: LocationMfaMode\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND state = 'new' AND (NOW() - created_at) > $2 * interval '1 second'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "mfa_mode: LocationMfaMode", + "type_info": { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + } + }, + { + "ordinal": 8, + "name": "state: VpnClientSessionState", + "type_info": { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int8", + "Float8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + false + ] + }, + "hash": "e9bfbd2e39ddc1cc0f95258cbd711fa4ea8d63de6e7bd7a0f5cf41119cb3bf86" +} From aa54802e16f2638b2a027060915d7c57522b3ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 20 Jan 2026 08:57:10 +0100 Subject: [PATCH 46/52] move migrations --- ...wn.sql => 20260119121935_[2.0.0]_vpn_client_sessions.down.sql} | 0 ...s.up.sql => 20260119121935_[2.0.0]_vpn_client_sessions.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename migrations/{20260115121935_[2.0.0]_vpn_client_sessions.down.sql => 20260119121935_[2.0.0]_vpn_client_sessions.down.sql} (100%) rename migrations/{20260115121935_[2.0.0]_vpn_client_sessions.up.sql => 20260119121935_[2.0.0]_vpn_client_sessions.up.sql} (100%) diff --git a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.down.sql b/migrations/20260119121935_[2.0.0]_vpn_client_sessions.down.sql similarity index 100% rename from migrations/20260115121935_[2.0.0]_vpn_client_sessions.down.sql rename to migrations/20260119121935_[2.0.0]_vpn_client_sessions.down.sql diff --git a/migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql b/migrations/20260119121935_[2.0.0]_vpn_client_sessions.up.sql similarity index 100% rename from migrations/20260115121935_[2.0.0]_vpn_client_sessions.up.sql rename to migrations/20260119121935_[2.0.0]_vpn_client_sessions.up.sql From 7631e45ceeadf5859b9f13e31b2ded15ba94aa9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 20 Jan 2026 08:57:18 +0100 Subject: [PATCH 47/52] update dependencies --- Cargo.lock | 78 ++--- flake.lock | 12 +- web/package.json | 20 +- web/pnpm-lock.yaml | 800 ++++++++++++++++++++++----------------------- 4 files changed, 455 insertions(+), 455 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41004bb79d..e9797f0ff9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,7 +217,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -459,7 +459,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -1150,7 +1150,7 @@ dependencies = [ "rustls-pki-types", "serde", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "x509-parser 0.18.0", ] @@ -1181,7 +1181,7 @@ dependencies = [ "serde_cbor_2", "sqlx", "struct-patch", - "thiserror 2.0.17", + "thiserror 2.0.18", "tonic", "totp-lite", "tracing", @@ -1246,7 +1246,7 @@ dependencies = [ "strum", "strum_macros", "tera", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -1280,7 +1280,7 @@ dependencies = [ "defguard_core", "serde_json", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1292,7 +1292,7 @@ dependencies = [ "defguard_core", "defguard_event_logger", "defguard_mail", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1311,7 +1311,7 @@ dependencies = [ "serde_json", "sqlx", "tera", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1345,7 +1345,7 @@ dependencies = [ "secrecy", "semver", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tonic", @@ -1359,7 +1359,7 @@ dependencies = [ "chrono", "defguard_common", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1373,7 +1373,7 @@ dependencies = [ "os_info", "semver", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "tonic", "tower", "tracing", @@ -1799,9 +1799,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixedbitset" @@ -2691,7 +2691,7 @@ dependencies = [ "num-bigint", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "yasna", "zeroize", ] @@ -2768,7 +2768,7 @@ dependencies = [ "native-tls", "nom 7.1.3", "percent-encoding", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-native-tls", "tokio-stream", @@ -4131,7 +4131,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4152,7 +4152,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4260,9 +4260,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" dependencies = [ "pem", "ring", @@ -4554,9 +4554,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -4564,9 +4564,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -4818,7 +4818,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -5003,7 +5003,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -5131,7 +5131,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -5215,7 +5215,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -5255,7 +5255,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -5281,7 +5281,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", "uuid", @@ -5551,11 +5551,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5571,9 +5571,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -6908,7 +6908,7 @@ dependencies = [ "oid-registry 0.8.1", "ring", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -7061,9 +7061,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" [[package]] name = "zopfli" diff --git a/flake.lock b/flake.lock index 1bd30e0f57..023f36f5b1 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768127708, - "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", + "lastModified": 1768564909, + "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", + "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1768359079, - "narHash": "sha256-a016mOfKconYrYo3fZLN6c2cnmqYYd44g2bUrBZAsQc=", + "lastModified": 1768877311, + "narHash": "sha256-abSDl0cNr0B+YCsIDpO1SjXD9JMxE4s8EFnhLEFVovI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "0357d1826057686637e41147545402cbbda420ce", + "rev": "59e4ab96304585fde3890025fd59bd2717985cc1", "type": "github" }, "original": { diff --git a/web/package.json b/web/package.json index 02380890a8..b127994a73 100644 --- a/web/package.json +++ b/web/package.json @@ -15,17 +15,17 @@ "dependencies": { "@axa-ch/react-polymorphic-types": "^1.4.1", "@floating-ui/react": "^0.27.16", - "@inlang/paraglide-js": "^2.8.0", + "@inlang/paraglide-js": "^2.9.0", "@react-hook/resize-observer": "^2.0.2", "@shortercode/webzip": "1.1.1-0", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", "@tanstack/react-devtools": "^0.9.2", "@tanstack/react-form": "^1.27.7", - "@tanstack/react-query": "^5.90.17", + "@tanstack/react-query": "^5.90.19", "@tanstack/react-query-devtools": "^5.91.2", - "@tanstack/react-router": "^1.149.3", - "@tanstack/react-router-devtools": "^1.149.3", + "@tanstack/react-router": "^1.153.2", + "@tanstack/react-router-devtools": "^1.153.2", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.18", "@uidotdev/usehooks": "^2.4.1", @@ -37,18 +37,18 @@ "humanize-duration": "^3.33.2", "ipaddr.js": "^2.3.0", "lodash-es": "^4.17.22", - "motion": "^12.26.2", + "motion": "^12.27.1", "qrcode.react": "^4.2.0", "qs": "^6.14.1", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-intersection-observer": "^10.0.0", + "react-intersection-observer": "^10.0.2", "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", "recharts": "^3.6.0", "rehype-raw": "^7.0.0", "rxjs": "^7.8.2", - "text-case": "^1.2.9", + "text-case": "^1.2.10", "zod": "^4.3.5", "zustand": "^5.0.10" }, @@ -56,18 +56,18 @@ "@biomejs/biome": "2.3.11", "@inlang/paraglide-js": "2.8.0", "@tanstack/devtools-vite": "^0.4.1", - "@tanstack/router-plugin": "^1.149.3", + "@tanstack/router-plugin": "^1.153.2", "@types/byte-size": "^8.1.2", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", - "@types/node": "^25.0.8", + "@types/node": "^25.0.9", "@types/qs": "^6.14.0", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.2", "autoprefixer": "^10.4.23", "globals": "^17.0.0", - "prettier": "^3.7.4", + "prettier": "^3.8.0", "sass": "^1.97.2", "sharp": "^0.34.5", "stylelint": "^16.26.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 788f93983a..2d5ced3e86 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^0.27.16 version: 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@inlang/paraglide-js': - specifier: ^2.8.0 - version: 2.8.0 + specifier: ^2.9.0 + version: 2.9.0 '@react-hook/resize-observer': specifier: ^2.0.2 version: 2.0.2(react@19.2.3) @@ -36,17 +36,17 @@ importers: specifier: ^1.27.7 version: 1.27.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-query': - specifier: ^5.90.17 - version: 5.90.17(react@19.2.3) + specifier: ^5.90.19 + version: 5.90.19(react@19.2.3) '@tanstack/react-query-devtools': specifier: ^5.91.2 - version: 5.91.2(@tanstack/react-query@5.90.17(react@19.2.3))(react@19.2.3) + version: 5.91.2(@tanstack/react-query@5.90.19(react@19.2.3))(react@19.2.3) '@tanstack/react-router': - specifier: ^1.149.3 - version: 1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.153.2 + version: 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': - specifier: ^1.149.3 - version: 1.149.3(@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.149.3)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.153.2 + version: 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.153.2)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -81,8 +81,8 @@ importers: specifier: ^4.17.22 version: 4.17.22 motion: - specifier: ^12.26.2 - version: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^12.27.1 + version: 12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) qrcode.react: specifier: ^4.2.0 version: 4.2.0(react@19.2.3) @@ -96,8 +96,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(react@19.2.3) react-intersection-observer: - specifier: ^10.0.0 - version: 10.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^10.0.2 + version: 10.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-loading-skeleton: specifier: ^3.5.0 version: 3.5.0(react@19.2.3) @@ -114,8 +114,8 @@ importers: specifier: ^7.8.2 version: 7.8.2 text-case: - specifier: ^1.2.9 - version: 1.2.9 + specifier: ^1.2.10 + version: 1.2.10 zod: specifier: ^4.3.5 version: 4.3.5 @@ -128,10 +128,10 @@ importers: version: 2.3.11 '@tanstack/devtools-vite': specifier: ^0.4.1 - version: 0.4.1(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)) + version: 0.4.1(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)) '@tanstack/router-plugin': - specifier: ^1.149.3 - version: 1.149.3(@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)) + specifier: ^1.153.2 + version: 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -142,8 +142,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^25.0.8 - version: 25.0.8 + specifier: ^25.0.9 + version: 25.0.9 '@types/qs': specifier: ^6.14.0 version: 6.14.0 @@ -155,7 +155,7 @@ importers: version: 19.2.3(@types/react@19.2.8) '@vitejs/plugin-react-swc': specifier: ^4.2.2 - version: 4.2.2(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)) + version: 4.2.2(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)) autoprefixer: specifier: ^10.4.23 version: 10.4.23(postcss@8.5.6) @@ -163,8 +163,8 @@ importers: specifier: ^17.0.0 version: 17.0.0 prettier: - specifier: ^3.7.4 - version: 3.7.4 + specifier: ^3.8.0 + version: 3.8.0 sass: specifier: ^1.97.2 version: 1.97.2 @@ -185,10 +185,10 @@ importers: version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) + version: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) vite-plugin-image-optimizer: specifier: ^2.0.3 - version: 2.0.3(sharp@0.34.5)(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)) + version: 2.0.3(sharp@0.34.5)(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)) packages: @@ -687,8 +687,8 @@ packages: cpu: [x64] os: [win32] - '@inlang/paraglide-js@2.8.0': - resolution: {integrity: sha512-ataaSmV53zz+tIr+KJLdC3tTB1uikS79hvtLlZk2ikbGRB/kcyQeg+lsqzjsXCAvy0/O28ucCRjxbHsTzOVQVg==} + '@inlang/paraglide-js@2.9.0': + resolution: {integrity: sha512-SDtemGiHuqwtBwprs3UEjb2K2HpAcG0km6nvmbVtd5SVwmgny2IqUyLynGBJ0F2s5Ntv2/cSeBLB59qXHynrVw==} hasBin: true '@inlang/recommend-sherlock@0.2.1': @@ -853,128 +853,128 @@ packages: '@rolldown/pluginutils@1.0.0-beta.47': resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} - '@rollup/rollup-android-arm-eabi@4.55.1': - resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} + '@rollup/rollup-android-arm-eabi@4.55.2': + resolution: {integrity: sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.55.1': - resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} + '@rollup/rollup-android-arm64@4.55.2': + resolution: {integrity: sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.55.1': - resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} + '@rollup/rollup-darwin-arm64@4.55.2': + resolution: {integrity: sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.55.1': - resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} + '@rollup/rollup-darwin-x64@4.55.2': + resolution: {integrity: sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.55.1': - resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} + '@rollup/rollup-freebsd-arm64@4.55.2': + resolution: {integrity: sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.55.1': - resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} + '@rollup/rollup-freebsd-x64@4.55.2': + resolution: {integrity: sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.55.1': - resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.55.2': + resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.55.1': - resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} + '@rollup/rollup-linux-arm-musleabihf@4.55.2': + resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.55.1': - resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} + '@rollup/rollup-linux-arm64-gnu@4.55.2': + resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.55.1': - resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} + '@rollup/rollup-linux-arm64-musl@4.55.2': + resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.55.1': - resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + '@rollup/rollup-linux-loong64-gnu@4.55.2': + resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loong64-musl@4.55.1': - resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} + '@rollup/rollup-linux-loong64-musl@4.55.2': + resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.55.1': - resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} + '@rollup/rollup-linux-ppc64-gnu@4.55.2': + resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-ppc64-musl@4.55.1': - resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + '@rollup/rollup-linux-ppc64-musl@4.55.2': + resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.55.1': - resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} + '@rollup/rollup-linux-riscv64-gnu@4.55.2': + resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.55.1': - resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} + '@rollup/rollup-linux-riscv64-musl@4.55.2': + resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.55.1': - resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} + '@rollup/rollup-linux-s390x-gnu@4.55.2': + resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.55.1': - resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} + '@rollup/rollup-linux-x64-gnu@4.55.2': + resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.55.1': - resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} + '@rollup/rollup-linux-x64-musl@4.55.2': + resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==} cpu: [x64] os: [linux] - '@rollup/rollup-openbsd-x64@4.55.1': - resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + '@rollup/rollup-openbsd-x64@4.55.2': + resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.55.1': - resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} + '@rollup/rollup-openharmony-arm64@4.55.2': + resolution: {integrity: sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.55.1': - resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} + '@rollup/rollup-win32-arm64-msvc@4.55.2': + resolution: {integrity: sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.55.1': - resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} + '@rollup/rollup-win32-ia32-msvc@4.55.2': + resolution: {integrity: sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.55.1': - resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} + '@rollup/rollup-win32-x64-gnu@4.55.2': + resolution: {integrity: sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.55.1': - resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} + '@rollup/rollup-win32-x64-msvc@4.55.2': + resolution: {integrity: sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==} cpu: [x64] os: [win32] @@ -1048,68 +1048,68 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@swc/core-darwin-arm64@1.15.8': - resolution: {integrity: sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==} + '@swc/core-darwin-arm64@1.15.10': + resolution: {integrity: sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.8': - resolution: {integrity: sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==} + '@swc/core-darwin-x64@1.15.10': + resolution: {integrity: sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.8': - resolution: {integrity: sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==} + '@swc/core-linux-arm-gnueabihf@1.15.10': + resolution: {integrity: sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.8': - resolution: {integrity: sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==} + '@swc/core-linux-arm64-gnu@1.15.10': + resolution: {integrity: sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.15.8': - resolution: {integrity: sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==} + '@swc/core-linux-arm64-musl@1.15.10': + resolution: {integrity: sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.15.8': - resolution: {integrity: sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==} + '@swc/core-linux-x64-gnu@1.15.10': + resolution: {integrity: sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.15.8': - resolution: {integrity: sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==} + '@swc/core-linux-x64-musl@1.15.10': + resolution: {integrity: sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.15.8': - resolution: {integrity: sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==} + '@swc/core-win32-arm64-msvc@1.15.10': + resolution: {integrity: sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.8': - resolution: {integrity: sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==} + '@swc/core-win32-ia32-msvc@1.15.10': + resolution: {integrity: sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.8': - resolution: {integrity: sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==} + '@swc/core-win32-x64-msvc@1.15.10': + resolution: {integrity: sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.8': - resolution: {integrity: sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==} + '@swc/core@1.15.10': + resolution: {integrity: sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1156,16 +1156,16 @@ packages: '@tanstack/form-core@1.27.7': resolution: {integrity: sha512-nvogpyE98fhb0NDw1Bf2YaCH+L7ZIUgEpqO9TkHucDn6zg3ni521boUpv0i8HKIrmmFwDYjWZoCnrgY4HYWTkw==} - '@tanstack/history@1.145.7': - resolution: {integrity: sha512-gMo/ReTUp0a3IOcZoI3hH6PLDC2R/5ELQ7P2yu9F6aEkA0wSQh+Q4qzMrtcKvF2ut0oE+16xWCGDo/TdYd6cEQ==} + '@tanstack/history@1.153.2': + resolution: {integrity: sha512-TVa0Wju5w6JZGq/S74Q7TQNtKXDatJaB4NYrhMZVU9ETlkgpr35NhDfOzsCJ93P0KCo1ZoDodlFp3c54/dLsyw==} engines: {node: '>=12'} '@tanstack/pacer-lite@0.1.1': resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.90.17': - resolution: {integrity: sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ==} + '@tanstack/query-core@5.90.19': + resolution: {integrity: sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA==} '@tanstack/query-devtools@5.92.0': resolution: {integrity: sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==} @@ -1194,25 +1194,25 @@ packages: '@tanstack/react-query': ^5.90.14 react: ^18 || ^19 - '@tanstack/react-query@5.90.17': - resolution: {integrity: sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ==} + '@tanstack/react-query@5.90.19': + resolution: {integrity: sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.149.3': - resolution: {integrity: sha512-QH16WA0NkfZSxku8fHy0CFm42MJ1mXeDnCAsaIZXKypv935MQzsXEvwn6ZZDkH8qP8eCQBoYlRVZmWiIr+9Omw==} + '@tanstack/react-router-devtools@1.153.2': + resolution: {integrity: sha512-LCEuRIyrF0tNKCBspR+TQj13MQ7sTCE4QkkuKAOp30nSdWLxq53bltnGs9bj/V/PTD52JibuAOYyxB94ssWZUA==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.149.3 - '@tanstack/router-core': ^1.149.3 + '@tanstack/react-router': ^1.153.2 + '@tanstack/router-core': ^1.153.2 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.149.3': - resolution: {integrity: sha512-yklZ2LSXLGfhW4PXu2N98yhGk8qtlkUbFRV42np0rx46s50wB5sXRkjdnqyGuDG/dldaBIi76M6vWg84Pmb4+A==} + '@tanstack/react-router@1.153.2': + resolution: {integrity: sha512-fAXUBA2gZAId7h2eSHsRcgTeF8pioUz8V5rrQ+IrvA0a6IsxhbTSKLYyqUg4jRDkkcUKtM8StKtvbZCY+0IYWw==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1237,30 +1237,30 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.149.3': - resolution: {integrity: sha512-obXmQ2hElxqjQ9cpABjXOvR/aQG+uG9ALEcVvyqP1ae57Fb3VhOuynmc2k/eVgx/bKKvxe2cqj4wCG04O0i5Zg==} + '@tanstack/router-core@1.153.2': + resolution: {integrity: sha512-WLaR+rSNW7bj9UCJQ3SKpuh6nZBZkpGnf2mpjn/uRB6joIQ3BU7aRdhb7w9Via/MP52iaHh5sd8NY3MaLpF2tQ==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.149.3': - resolution: {integrity: sha512-hgGPqqs/yD2XgmyTdmwBH6FrXnMbcsNWLup7nHPp/NGod9mtGKqSR2gBpicjZTBpaX/ihX29GG1s0l5MKmpQXA==} + '@tanstack/router-devtools-core@1.153.2': + resolution: {integrity: sha512-53gFlnz2oUeGvRwu7hzi+jlqm5F5X1XwNniirCTjggsV5P+FVQ7YJ+gfMuN5MHonWmVCLd1QqGkl2nYRTGHeTg==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.149.3 + '@tanstack/router-core': ^1.153.2 csstype: ^3.0.10 peerDependenciesMeta: csstype: optional: true - '@tanstack/router-generator@1.149.3': - resolution: {integrity: sha512-SKjsYiXT81wO2V1wF+7ITc/EDvt1xsN4JO5MvFYFL1s5Pq1gCW7/nI4yvqZzGm7aE26J8TLdTn419GfuE9SIlw==} + '@tanstack/router-generator@1.153.2': + resolution: {integrity: sha512-bEhmCtXq5vv3HukKq5zmTDBNDRqVllYxsHoWtqEvHv5hCb5xwKKfUMGemRoiQ96/wLFuGnA5DYkem2GZWcG3wg==} engines: {node: '>=12'} - '@tanstack/router-plugin@1.149.3': - resolution: {integrity: sha512-OO+S5czNWGC/+IC6XtKwPkqkRRaVJna6P8jNu+trV1hByhl1NKvPFRFBIqvUlMsM100hAesb0Jk2LsKwleywNA==} + '@tanstack/router-plugin@1.153.2': + resolution: {integrity: sha512-aMMc70ChM0wBYOToq39kTMKI2A0EKWpumiKTJyAwEglXf0raF48+26Fmv0gr9/5CLvD0g8ljllsskVDyzg8oDw==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.149.3 + '@tanstack/react-router': ^1.153.2 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' vite-plugin-solid: ^2.11.10 webpack: '>=5.92.0' @@ -1354,8 +1354,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.0.8': - resolution: {integrity: sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==} + '@types/node@25.0.9': + resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1461,8 +1461,8 @@ packages: balanced-match@2.0.0: resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} - baseline-browser-mapping@2.9.14: - resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} + baseline-browser-mapping@2.9.15: + resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} hasBin: true binary-extensions@2.3.0: @@ -1487,8 +1487,8 @@ packages: '@75lb/nature': optional: true - cacheable@2.3.1: - resolution: {integrity: sha512-yr+FSHWn1ZUou5LkULX/S+jhfgfnLbuKQjE40tyEd4fxGZVMbBL5ifno0J0OauykS8UiCSgHi+DV/YD+rjFxFg==} + cacheable@2.3.2: + resolution: {integrity: sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==} call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} @@ -1502,8 +1502,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001764: - resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} + caniuse-lite@1.0.30001765: + resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1611,8 +1611,8 @@ packages: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} - d3-format@3.1.1: - resolution: {integrity: sha512-ryitBnaRbXQtgZ/gU50GSn6jQRwinSCQclpakXymvLd8ytTgE5bmSfgYcUxD7XYL34qHhFDyVk71qqKsfSyvmA==} + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} engines: {node: '>=12'} d3-interpolate@3.0.1: @@ -1658,8 +1658,8 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - decode-named-character-reference@1.2.0: - resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} @@ -1732,8 +1732,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-toolkit@1.43.0: - resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} + es-toolkit@1.44.0: + resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} @@ -1752,8 +1752,8 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1784,15 +1784,15 @@ packages: picomatch: optional: true - file-entry-cache@11.1.1: - resolution: {integrity: sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==} + file-entry-cache@11.1.2: + resolution: {integrity: sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - flat-cache@6.1.19: - resolution: {integrity: sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==} + flat-cache@6.1.20: + resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -1813,8 +1813,8 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.26.2: - resolution: {integrity: sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA==} + framer-motion@12.27.1: + resolution: {integrity: sha512-cEAqO69kcZt3gL0TGua8WTgRQfv4J57nqt1zxHtLKwYhAwA0x9kDS/JbMa1hJbwkGY74AGJKvZ9pX/IqWZtZWQ==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2025,8 +2025,8 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} - isbot@5.1.32: - resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==} + isbot@5.1.33: + resolution: {integrity: sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg==} engines: {node: '>=18'} isexe@2.0.0: @@ -2210,14 +2210,14 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - motion-dom@12.26.2: - resolution: {integrity: sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==} + motion-dom@12.27.1: + resolution: {integrity: sha512-V/53DA2nBqKl9O2PMJleSUb/G0dsMMeZplZwgIQf5+X0bxIu7Q1cTv6DrjvTTGYRm3+7Y5wMlRZ1wT61boU/bQ==} motion-utils@12.24.10: resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} - motion@12.26.2: - resolution: {integrity: sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw==} + motion@12.27.1: + resolution: {integrity: sha512-FAZTPDm1LccBdWSL46WLnEdTSHmdVx+fdWK8f61qBQn67MmFefXLXlrwy94rK2DDsd9A50Gj8H+LYCgQ/cQlFg==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2313,8 +2313,8 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + prettier@3.8.0: + resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} engines: {node: '>=14'} hasBin: true @@ -2324,8 +2324,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - qified@0.5.3: - resolution: {integrity: sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==} + qified@0.6.0: + resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} engines: {node: '>=20'} qrcode.react@4.2.0: @@ -2345,8 +2345,8 @@ packages: peerDependencies: react: ^19.2.3 - react-intersection-observer@10.0.0: - resolution: {integrity: sha512-JJRgcnFQoVXmbE5+GXr1OS1NDD1gHk0HyfpLcRf0575IbJz+io8yzs4mWVlfaqOQq1FiVjLvuYAdEEcrrCfveg==} + react-intersection-observer@10.0.2: + resolution: {integrity: sha512-lAMzxVWrBko6SLd1jx6l84fVrzJu91hpxHlvD2as2Wec9mDCjdYXwc5xNOFBchpeBir0Y7AGBW+C/AYMa7CSFg==} peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2443,8 +2443,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.55.1: - resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} + rollup@4.55.2: + resolution: {integrity: sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2628,68 +2628,68 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - text-camel-case@1.2.9: - resolution: {integrity: sha512-wKYs9SgRxYizJE1mneR7BbLNlGw2IYzJAS8XwkWIry0CTbO1gvvPkFsx5Z1/hr+VqUaBqx9q3yKd30HpZLdMsQ==} + text-camel-case@1.2.10: + resolution: {integrity: sha512-KNrWeZzQT+gh73V1LnmgTkjK7V+tMRjLCc6VrGwkqbiRdnGVIWBUgIvVnvnaVCxIvZ/2Ke8DCmgPirlQcCqD3Q==} - text-capital-case@1.2.9: - resolution: {integrity: sha512-X5zV8U8pxtq2xS2t46lgAWqZdDbgWMKq03MQSNwY2CJdQCsdTNh144E2Q/q9wBxWzSBUXn+jRc9kF+Gs8/pGhA==} + text-capital-case@1.2.10: + resolution: {integrity: sha512-yvViUJKSSQcRO58je224bhPHg/Hij9MEY43zuKShtFzrPwW/fOAarUJ5UkTMSB81AOO1m8q+JiFdxMF4etKZbA==} - text-case@1.2.9: - resolution: {integrity: sha512-zZVdA8rMcjx9zhekdUuOPZShc25UTV7W8/ddKbgbPtfCEvIiToPtWiSd2lXLSuiGMovNhJ4+Tw49xll9o9ts+Q==} + text-case@1.2.10: + resolution: {integrity: sha512-5bY3Ks/u7OJ5YO69iyXrG5Xf2wUZeyko7U78nPUnYoSeuNeAfA5uAix5hTspfkl6smm3yCBObrex+kFvzeIcJg==} - text-constant-case@1.2.9: - resolution: {integrity: sha512-Vosm6nC7Gag+JFakJHwqS9AXRNgl07j5KZ7srU9cYuKRzYwrxzeJ4RpEogRBNHw7CfmOm0j5FGEznblWtu7pIw==} + text-constant-case@1.2.10: + resolution: {integrity: sha512-/OfU798O2wrwKN9kQf71WhJeAlklGnbby0Tupp+Ez9NXymW+6oF9LWDRTkN+OreTmHucdvp4WQd6O5Rah5zj8A==} - text-dot-case@1.2.9: - resolution: {integrity: sha512-N83hsnvGdSO9q9AfNSB9Cy1LFDNN2MCx53LcxtaPoDWPUTk47fv0JlvIY1tgY0wyzCiThF03kVj3jworvAOScA==} + text-dot-case@1.2.10: + resolution: {integrity: sha512-vf4xguy5y6e39RlDZeWZFMDf2mNkR23VTSVb9e68dUSpfJscG9/1YWWpW3n8TinzQxBZlsn5sT5olL33MvvQXw==} - text-header-case@1.2.9: - resolution: {integrity: sha512-TqryEKcYisQAfWLbtT3xPnZlMZ/mySO1uS+LUg+B0eNuqgETrSzVpXIUj5E6Zf/EyJHgpZf4VndbAXtOMJuT4w==} + text-header-case@1.2.10: + resolution: {integrity: sha512-sVb1NY9bwxtu+Z7CVyWbr+I0AkWtF0kEHL/Zz5V2u/WdkjK5tKBwl5nXf0NGy9da4ZUYTBb+TmQpOIqihzvFMQ==} - text-is-lower-case@1.2.9: - resolution: {integrity: sha512-cEurrWSnYVYqL8FSwl5cK4mdfqF7qNDCcKJgXI3NnfTesiB8umxAhdlQoErrRYI1xEvYr2WN0MI333EehUhQjg==} + text-is-lower-case@1.2.10: + resolution: {integrity: sha512-dMTeTgrdWWfYf3fKxvjMkDPuXWv96cWbd1Uym6Zjv9H855S1uHxjkFsGbTYJ2tEK0NvAylRySTQlI6axlcMc4w==} - text-is-upper-case@1.2.9: - resolution: {integrity: sha512-HxsWr3VCsXXiLlhD0c+Ey+mS2lOTCiSJbkepjaXNHl2bp33KiscQaiG0qLwQmmpZQm4SJCg2s9FkndxS0RNDLQ==} + text-is-upper-case@1.2.10: + resolution: {integrity: sha512-PGD/cXoXECGAY1HVZxDdmpJUW2ZUAKQ6DTamDfCHC9fc/z4epOz0pB/ThBnjJA3fz+d2ApkMjAfZDjuZFcodzg==} - text-kebab-case@1.2.9: - resolution: {integrity: sha512-nOUyNR5Ej2B9D/wyyXfwUEv26+pQuOb1pEX+ojE37mCIWo8QeOxw5y6nxuqDmG7NrEPzbO6265UMV+EICH13Cw==} + text-kebab-case@1.2.10: + resolution: {integrity: sha512-3XZJAApx5JQpUO7eXo7GQ2TyRcGw3OVbqxz6QJb2h+N8PbLLbz3zJVeXdGrhTkoUIbkSZ6PmHx6LRDaHXTdMcA==} - text-lower-case-first@1.2.9: - resolution: {integrity: sha512-iiphHTV7PVH0MljrEQUA9iBE7jfDpXoi4RQju3WzZU3BRVbS6540cNZgxR19hWa0z6z/7cJTH0Ls9LPBaiUfKg==} + text-lower-case-first@1.2.10: + resolution: {integrity: sha512-Oro84jZPDLD9alfdZWmtFHYTvCaaSz2o4thPtjMsK4GAkTyVg9juYXWj0y0YFyjLYGH69muWsBe4/MR5S7iolw==} - text-lower-case@1.2.9: - resolution: {integrity: sha512-53AOnDrhPpiAUQkgY1SHleKUXp/u7GsqRX13NcCREZscmtjLLJ099uxMRjkK7q2KwHkFYVPl9ytkQlTkTQLS0w==} + text-lower-case@1.2.10: + resolution: {integrity: sha512-c9j5pIAN3ObAp1+4R7970e1bgtahTRF/5ZQdX2aJBuBngYTYZZIck0NwFXUKk5BnYpLGsre5KFHvpqvf4IYKgg==} - text-no-case@1.2.9: - resolution: {integrity: sha512-IcCt328KaapimSrytP4ThfC8URmHZb2DgOqCL9BYvGjpxY2lDiqCkIQk9sClZtwcELs2gTnq83a7jNc573FTLA==} + text-no-case@1.2.10: + resolution: {integrity: sha512-4/m79pzQrywrwEG5lCULY1lQvFY+EKjhH9xSMT6caPK5plqzm9Y7rXyv+UXPd3s9qH6QODZnvsAYWW3M0JgxRA==} - text-param-case@1.2.9: - resolution: {integrity: sha512-nR/Ju9amY3aQS1en2CUCgqN/ZiZIVdDyjlJ3xX5J92ChBevGuA4o9K10fh3JGMkbzK97Vcb+bWQJ4Q+Svz+GyQ==} + text-param-case@1.2.10: + resolution: {integrity: sha512-hkavcLsRRzZcGryPAshct1AwIOMj/FexYjMaLpGZCYYBn1lcZEeyMzJZPSckzkOYpq35LYSQr3xZto9XU5OAsw==} - text-pascal-case@1.2.9: - resolution: {integrity: sha512-o6ZxMGjWDTUW54pcghpXes+C2PqbYRMdU5mHrIhueb6z6nq1NueiIOeCUdrSjN/3wXfhCmnFjK7/d9aRGZNqSg==} + text-pascal-case@1.2.10: + resolution: {integrity: sha512-/kynZD8vTYOmm/RECjIDaz3qYEUZc/N/bnC79XuAFxwXjdNVjj/jGovKJLRzqsYK/39N22XpGcVmGg7yIrbk6w==} - text-path-case@1.2.9: - resolution: {integrity: sha512-s8cJ6r5TkJp5ticXMgtxd7f12odEN4d1CfX5u4aoz6jcUtBR2lDqzIhVimkqWFMJ4UKPSrmilUha8Xc2BPi+ow==} + text-path-case@1.2.10: + resolution: {integrity: sha512-vbKdRCaVEeOaW6sm24QP9NbH7TS9S4ZQ3u19H8eylDox7m2HtFwYIBjAPv+v3z4I/+VjrMy9LB54lNP1uEqRHw==} - text-sentence-case@1.2.9: - resolution: {integrity: sha512-/G/Yi5kZfUa1edFRV4O3lGZAkbDZTFvlwW8CYfH7szkEGe2k2MYEYbOyAkGRVQEGV6V6JiuUAaP3VS9c1tB6nQ==} + text-sentence-case@1.2.10: + resolution: {integrity: sha512-NO4MRlbfxFhl9QgQLuCL4xHmvE7PUWHVPWsZxQ5nzRtDjXOUllWvtsvl8CP5tBEvBmzg0kwfflxfhRtr5vBQGg==} - text-snake-case@1.2.9: - resolution: {integrity: sha512-+ZrqK19ynF/TLQZ7ynqVrL2Dy04uu9syYZwsm8PhzUdsY3XrwPy6QiRqhIEFqhyWbShPcfyfmheer5UEQqFxlw==} + text-snake-case@1.2.10: + resolution: {integrity: sha512-6ttMZ+B9jkHKun908HYr4xSvEtlbfJJ4MvpQ06JEKRGhwjMI0x8t2Wywp+MEzN6142O6E/zKhra18KyBL6cvXA==} - text-swap-case@1.2.9: - resolution: {integrity: sha512-g5fp12ldktYKK9wdHRMvvtSCQrZYNv/D+ZGLumDsvAY4q9T5bCMO2IWMkIP1F5gVQrysdHH6Xv877P/pjUq1iw==} + text-swap-case@1.2.10: + resolution: {integrity: sha512-vO3jwInIk0N77oEFakYZ2Hn/llTmRwf2c3RvkX/LfvmLWVp+3QcIc6bwUEtbqGQ5Xh2okjFhYrfkHZstVc3N4Q==} - text-title-case@1.2.9: - resolution: {integrity: sha512-RAtC9cdmPp41ns5/HXZBsaQg71BsHT7uZpj2ojTtuFa8o2dNuRYYOrSmy5YdLRIAJQ6WK5hQVpV3jHuq7a+4Tw==} + text-title-case@1.2.10: + resolution: {integrity: sha512-bqA+WWexUMWu9A3fdNar+3GXXW+c5xOvMyuK5hOx/w0AlqhyQptyCrMFjGB8Fd9dxbryBNmJ+5rWtC1OBDxlaA==} - text-upper-case-first@1.2.9: - resolution: {integrity: sha512-wEDD1B6XqJmEV+xEnBJd+2sBCHZ+7fvA/8Rv/o8+dAsp05YWjYP/kjB8sPH6zqzW0s6jtehIg4IlcKjcYxk2CQ==} + text-upper-case-first@1.2.10: + resolution: {integrity: sha512-VXs7j7BbpKwvolDh5fwpYRmMrUHGkxbY8E90fhBzKUoKfadvWmPT/jFieoZ4UPLzr208pXvQEFbb2zO9Qzs9Fg==} - text-upper-case@1.2.9: - resolution: {integrity: sha512-K/0DNT7a4z8eah2spARtoJllTZyrNTo6Uc0ujhN/96Ir9uJ/slpahfs13y46H9osL3daaLl3O7iXOkW4xtX6bg==} + text-upper-case@1.2.10: + resolution: {integrity: sha512-L1AtZ8R+jtSMTq0Ffma9R4Rzbrc3iuYW89BmWFH41AwnDfRmEBlBOllm1ZivRLQ/6pEu2p+3XKBHx9fsMl2CWg==} tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3284,7 +3284,7 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inlang/paraglide-js@2.8.0': + '@inlang/paraglide-js@2.9.0': dependencies: '@inlang/recommend-sherlock': 0.2.1 '@inlang/sdk': 2.6.0 @@ -3452,79 +3452,79 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rollup/rollup-android-arm-eabi@4.55.1': + '@rollup/rollup-android-arm-eabi@4.55.2': optional: true - '@rollup/rollup-android-arm64@4.55.1': + '@rollup/rollup-android-arm64@4.55.2': optional: true - '@rollup/rollup-darwin-arm64@4.55.1': + '@rollup/rollup-darwin-arm64@4.55.2': optional: true - '@rollup/rollup-darwin-x64@4.55.1': + '@rollup/rollup-darwin-x64@4.55.2': optional: true - '@rollup/rollup-freebsd-arm64@4.55.1': + '@rollup/rollup-freebsd-arm64@4.55.2': optional: true - '@rollup/rollup-freebsd-x64@4.55.1': + '@rollup/rollup-freebsd-x64@4.55.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + '@rollup/rollup-linux-arm-gnueabihf@4.55.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.55.1': + '@rollup/rollup-linux-arm-musleabihf@4.55.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.55.1': + '@rollup/rollup-linux-arm64-gnu@4.55.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.55.1': + '@rollup/rollup-linux-arm64-musl@4.55.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.55.1': + '@rollup/rollup-linux-loong64-gnu@4.55.2': optional: true - '@rollup/rollup-linux-loong64-musl@4.55.1': + '@rollup/rollup-linux-loong64-musl@4.55.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.55.1': + '@rollup/rollup-linux-ppc64-gnu@4.55.2': optional: true - '@rollup/rollup-linux-ppc64-musl@4.55.1': + '@rollup/rollup-linux-ppc64-musl@4.55.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.55.1': + '@rollup/rollup-linux-riscv64-gnu@4.55.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.55.1': + '@rollup/rollup-linux-riscv64-musl@4.55.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.55.1': + '@rollup/rollup-linux-s390x-gnu@4.55.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.55.1': + '@rollup/rollup-linux-x64-gnu@4.55.2': optional: true - '@rollup/rollup-linux-x64-musl@4.55.1': + '@rollup/rollup-linux-x64-musl@4.55.2': optional: true - '@rollup/rollup-openbsd-x64@4.55.1': + '@rollup/rollup-openbsd-x64@4.55.2': optional: true - '@rollup/rollup-openharmony-arm64@4.55.1': + '@rollup/rollup-openharmony-arm64@4.55.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.55.1': + '@rollup/rollup-win32-arm64-msvc@4.55.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.55.1': + '@rollup/rollup-win32-ia32-msvc@4.55.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.55.1': + '@rollup/rollup-win32-x64-gnu@4.55.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.55.1': + '@rollup/rollup-win32-x64-msvc@4.55.2': optional: true '@shortercode/webzip@1.1.1-0': {} @@ -3598,51 +3598,51 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@swc/core-darwin-arm64@1.15.8': + '@swc/core-darwin-arm64@1.15.10': optional: true - '@swc/core-darwin-x64@1.15.8': + '@swc/core-darwin-x64@1.15.10': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.8': + '@swc/core-linux-arm-gnueabihf@1.15.10': optional: true - '@swc/core-linux-arm64-gnu@1.15.8': + '@swc/core-linux-arm64-gnu@1.15.10': optional: true - '@swc/core-linux-arm64-musl@1.15.8': + '@swc/core-linux-arm64-musl@1.15.10': optional: true - '@swc/core-linux-x64-gnu@1.15.8': + '@swc/core-linux-x64-gnu@1.15.10': optional: true - '@swc/core-linux-x64-musl@1.15.8': + '@swc/core-linux-x64-musl@1.15.10': optional: true - '@swc/core-win32-arm64-msvc@1.15.8': + '@swc/core-win32-arm64-msvc@1.15.10': optional: true - '@swc/core-win32-ia32-msvc@1.15.8': + '@swc/core-win32-ia32-msvc@1.15.10': optional: true - '@swc/core-win32-x64-msvc@1.15.8': + '@swc/core-win32-x64-msvc@1.15.10': optional: true - '@swc/core@1.15.8': + '@swc/core@1.15.10': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.8 - '@swc/core-darwin-x64': 1.15.8 - '@swc/core-linux-arm-gnueabihf': 1.15.8 - '@swc/core-linux-arm64-gnu': 1.15.8 - '@swc/core-linux-arm64-musl': 1.15.8 - '@swc/core-linux-x64-gnu': 1.15.8 - '@swc/core-linux-x64-musl': 1.15.8 - '@swc/core-win32-arm64-msvc': 1.15.8 - '@swc/core-win32-ia32-msvc': 1.15.8 - '@swc/core-win32-x64-msvc': 1.15.8 + '@swc/core-darwin-arm64': 1.15.10 + '@swc/core-darwin-x64': 1.15.10 + '@swc/core-linux-arm-gnueabihf': 1.15.10 + '@swc/core-linux-arm64-gnu': 1.15.10 + '@swc/core-linux-arm64-musl': 1.15.10 + '@swc/core-linux-x64-gnu': 1.15.10 + '@swc/core-linux-x64-musl': 1.15.10 + '@swc/core-win32-arm64-msvc': 1.15.10 + '@swc/core-win32-ia32-msvc': 1.15.10 + '@swc/core-win32-x64-msvc': 1.15.10 '@swc/counter@0.1.3': {} @@ -3671,7 +3671,7 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.4.1(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0))': + '@tanstack/devtools-vite@0.4.1(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.6 '@babel/generator': 7.28.6 @@ -3683,7 +3683,7 @@ snapshots: chalk: 5.6.2 launch-editor: 2.12.0 picomatch: 4.0.3 - vite: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) transitivePeerDependencies: - bufferutil - supports-color @@ -3711,11 +3711,11 @@ snapshots: '@tanstack/pacer-lite': 0.1.1 '@tanstack/store': 0.7.7 - '@tanstack/history@1.145.7': {} + '@tanstack/history@1.153.2': {} '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.90.17': {} + '@tanstack/query-core@5.90.19': {} '@tanstack/query-devtools@5.92.0': {} @@ -3740,34 +3740,34 @@ snapshots: transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.17(react@19.2.3))(react@19.2.3)': + '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.19(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/query-devtools': 5.92.0 - '@tanstack/react-query': 5.90.17(react@19.2.3) + '@tanstack/react-query': 5.90.19(react@19.2.3) react: 19.2.3 - '@tanstack/react-query@5.90.17(react@19.2.3)': + '@tanstack/react-query@5.90.19(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.90.17 + '@tanstack/query-core': 5.90.19 react: 19.2.3 - '@tanstack/react-router-devtools@1.149.3(@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.149.3)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router-devtools@1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.153.2)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-devtools-core': 1.149.3(@tanstack/router-core@1.149.3)(csstype@3.2.3) + '@tanstack/react-router': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-devtools-core': 1.153.2(@tanstack/router-core@1.153.2)(csstype@3.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@tanstack/router-core': 1.149.3 + '@tanstack/router-core': 1.153.2 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/history': 1.145.7 + '@tanstack/history': 1.153.2 '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.149.3 - isbot: 5.1.32 + '@tanstack/router-core': 1.153.2 + isbot: 5.1.33 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) tiny-invariant: 1.3.3 @@ -3792,9 +3792,9 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@tanstack/router-core@1.149.3': + '@tanstack/router-core@1.153.2': dependencies: - '@tanstack/history': 1.145.7 + '@tanstack/history': 1.153.2 '@tanstack/store': 0.8.0 cookie-es: 2.0.0 seroval: 1.4.2 @@ -3802,21 +3802,21 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.149.3(@tanstack/router-core@1.149.3)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.153.2(@tanstack/router-core@1.153.2)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.149.3 + '@tanstack/router-core': 1.153.2 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.149.3': + '@tanstack/router-generator@1.153.2': dependencies: - '@tanstack/router-core': 1.149.3 + '@tanstack/router-core': 1.153.2 '@tanstack/router-utils': 1.143.11 '@tanstack/virtual-file-routes': 1.145.4 - prettier: 3.7.4 + prettier: 3.8.0 recast: 0.23.11 source-map: 0.7.6 tsx: 4.21.0 @@ -3824,7 +3824,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.149.3(@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0))': + '@tanstack/router-plugin@1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6) @@ -3832,8 +3832,8 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.28.6 '@babel/types': 7.28.6 - '@tanstack/router-core': 1.149.3 - '@tanstack/router-generator': 1.149.3 + '@tanstack/router-core': 1.153.2 + '@tanstack/router-generator': 1.153.2 '@tanstack/router-utils': 1.143.11 '@tanstack/virtual-file-routes': 1.145.4 babel-dead-code-elimination: 1.0.12 @@ -3841,8 +3841,8 @@ snapshots: unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - vite: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) + '@tanstack/react-router': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + vite: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -3922,7 +3922,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.0.8': + '@types/node@25.0.9': dependencies: undici-types: 7.16.0 @@ -3949,11 +3949,11 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@4.2.2(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.47 - '@swc/core': 1.15.8 - vite: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) + '@swc/core': 1.15.10 + vite: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) transitivePeerDependencies: - '@swc/helpers' @@ -3998,7 +3998,7 @@ snapshots: autoprefixer@10.4.23(postcss@8.5.6): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001764 + caniuse-lite: 1.0.30001765 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 @@ -4025,7 +4025,7 @@ snapshots: balanced-match@2.0.0: {} - baseline-browser-mapping@2.9.14: {} + baseline-browser-mapping@2.9.15: {} binary-extensions@2.3.0: {} @@ -4035,21 +4035,21 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.14 - caniuse-lite: 1.0.30001764 + baseline-browser-mapping: 2.9.15 + caniuse-lite: 1.0.30001765 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) byte-size@9.0.1: {} - cacheable@2.3.1: + cacheable@2.3.2: dependencies: '@cacheable/memory': 2.0.7 '@cacheable/utils': 2.3.3 hookified: 1.15.0 keyv: 5.5.5 - qified: 0.5.3 + qified: 0.6.0 call-bind-apply-helpers@1.0.2: dependencies: @@ -4063,7 +4063,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001764: {} + caniuse-lite@1.0.30001765: {} ccount@2.0.1: {} @@ -4153,7 +4153,7 @@ snapshots: d3-ease@3.0.1: {} - d3-format@3.1.1: {} + d3-format@3.1.2: {} d3-interpolate@3.0.1: dependencies: @@ -4164,7 +4164,7 @@ snapshots: d3-scale@4.0.2: dependencies: d3-array: 3.2.4 - d3-format: 3.1.1 + d3-format: 3.1.2 d3-interpolate: 3.0.1 d3-time: 3.1.0 d3-time-format: 4.1.0 @@ -4191,7 +4191,7 @@ snapshots: decimal.js-light@2.5.1: {} - decode-named-character-reference@1.2.0: + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -4248,7 +4248,7 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - es-toolkit@1.43.0: {} + es-toolkit@1.44.0: {} esbuild@0.27.2: optionalDependencies: @@ -4285,7 +4285,7 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} - eventemitter3@5.0.1: {} + eventemitter3@5.0.4: {} extend@3.0.2: {} @@ -4311,17 +4311,17 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - file-entry-cache@11.1.1: + file-entry-cache@11.1.2: dependencies: - flat-cache: 6.1.19 + flat-cache: 6.1.20 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - flat-cache@6.1.19: + flat-cache@6.1.20: dependencies: - cacheable: 2.3.1 + cacheable: 2.3.2 flatted: 3.3.3 hookified: 1.15.0 @@ -4339,9 +4339,9 @@ snapshots: fraction.js@5.3.4: {} - framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.26.2 + motion-dom: 12.27.1 motion-utils: 12.24.10 tslib: 2.8.1 optionalDependencies: @@ -4567,7 +4567,7 @@ snapshots: is-plain-object@5.0.0: {} - isbot@5.1.32: {} + isbot@5.1.33: {} isexe@2.0.0: {} @@ -4622,7 +4622,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 micromark: 4.0.2 @@ -4717,7 +4717,7 @@ snapshots: micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-factory-destination: 2.0.1 micromark-factory-label: 2.0.1 @@ -4792,7 +4792,7 @@ snapshots: micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 micromark-util-character: 2.1.1 micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 @@ -4830,7 +4830,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 debug: 4.4.3 - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 @@ -4859,15 +4859,15 @@ snapshots: dependencies: mime-db: 1.52.0 - motion-dom@12.26.2: + motion-dom@12.27.1: dependencies: motion-utils: 12.24.10 motion-utils@12.24.10: {} - motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + motion@12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 optionalDependencies: react: 19.2.3 @@ -4895,7 +4895,7 @@ snapshots: '@types/unist': 2.0.11 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 @@ -4946,13 +4946,13 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.7.4: {} + prettier@3.8.0: {} property-information@7.1.0: {} proxy-from-env@1.1.0: {} - qified@0.5.3: + qified@0.6.0: dependencies: hookified: 1.15.0 @@ -4971,7 +4971,7 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 - react-intersection-observer@10.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-intersection-observer@10.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 optionalDependencies: @@ -5031,8 +5031,8 @@ snapshots: '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3) clsx: 2.1.1 decimal.js-light: 2.5.1 - es-toolkit: 1.43.0 - eventemitter3: 5.0.1 + es-toolkit: 1.44.0 + eventemitter3: 5.0.4 immer: 10.2.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5087,35 +5087,35 @@ snapshots: reusify@1.1.0: {} - rollup@4.55.1: + rollup@4.55.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.55.1 - '@rollup/rollup-android-arm64': 4.55.1 - '@rollup/rollup-darwin-arm64': 4.55.1 - '@rollup/rollup-darwin-x64': 4.55.1 - '@rollup/rollup-freebsd-arm64': 4.55.1 - '@rollup/rollup-freebsd-x64': 4.55.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 - '@rollup/rollup-linux-arm-musleabihf': 4.55.1 - '@rollup/rollup-linux-arm64-gnu': 4.55.1 - '@rollup/rollup-linux-arm64-musl': 4.55.1 - '@rollup/rollup-linux-loong64-gnu': 4.55.1 - '@rollup/rollup-linux-loong64-musl': 4.55.1 - '@rollup/rollup-linux-ppc64-gnu': 4.55.1 - '@rollup/rollup-linux-ppc64-musl': 4.55.1 - '@rollup/rollup-linux-riscv64-gnu': 4.55.1 - '@rollup/rollup-linux-riscv64-musl': 4.55.1 - '@rollup/rollup-linux-s390x-gnu': 4.55.1 - '@rollup/rollup-linux-x64-gnu': 4.55.1 - '@rollup/rollup-linux-x64-musl': 4.55.1 - '@rollup/rollup-openbsd-x64': 4.55.1 - '@rollup/rollup-openharmony-arm64': 4.55.1 - '@rollup/rollup-win32-arm64-msvc': 4.55.1 - '@rollup/rollup-win32-ia32-msvc': 4.55.1 - '@rollup/rollup-win32-x64-gnu': 4.55.1 - '@rollup/rollup-win32-x64-msvc': 4.55.1 + '@rollup/rollup-android-arm-eabi': 4.55.2 + '@rollup/rollup-android-arm64': 4.55.2 + '@rollup/rollup-darwin-arm64': 4.55.2 + '@rollup/rollup-darwin-x64': 4.55.2 + '@rollup/rollup-freebsd-arm64': 4.55.2 + '@rollup/rollup-freebsd-x64': 4.55.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.2 + '@rollup/rollup-linux-arm-musleabihf': 4.55.2 + '@rollup/rollup-linux-arm64-gnu': 4.55.2 + '@rollup/rollup-linux-arm64-musl': 4.55.2 + '@rollup/rollup-linux-loong64-gnu': 4.55.2 + '@rollup/rollup-linux-loong64-musl': 4.55.2 + '@rollup/rollup-linux-ppc64-gnu': 4.55.2 + '@rollup/rollup-linux-ppc64-musl': 4.55.2 + '@rollup/rollup-linux-riscv64-gnu': 4.55.2 + '@rollup/rollup-linux-riscv64-musl': 4.55.2 + '@rollup/rollup-linux-s390x-gnu': 4.55.2 + '@rollup/rollup-linux-x64-gnu': 4.55.2 + '@rollup/rollup-linux-x64-musl': 4.55.2 + '@rollup/rollup-openbsd-x64': 4.55.2 + '@rollup/rollup-openharmony-arm64': 4.55.2 + '@rollup/rollup-win32-arm64-msvc': 4.55.2 + '@rollup/rollup-win32-ia32-msvc': 4.55.2 + '@rollup/rollup-win32-x64-gnu': 4.55.2 + '@rollup/rollup-win32-x64-msvc': 4.55.2 fsevents: 2.3.3 run-parallel@1.2.0: @@ -5319,7 +5319,7 @@ snapshots: debug: 4.4.3 fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 - file-entry-cache: 11.1.1 + file-entry-cache: 11.1.2 global-modules: 2.0.0 globby: 11.1.0 globjoin: 0.1.4 @@ -5369,98 +5369,98 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - text-camel-case@1.2.9: + text-camel-case@1.2.10: dependencies: - text-pascal-case: 1.2.9 + text-pascal-case: 1.2.10 - text-capital-case@1.2.9: + text-capital-case@1.2.10: dependencies: - text-no-case: 1.2.9 - text-upper-case-first: 1.2.9 + text-no-case: 1.2.10 + text-upper-case-first: 1.2.10 - text-case@1.2.9: + text-case@1.2.10: dependencies: - text-camel-case: 1.2.9 - text-capital-case: 1.2.9 - text-constant-case: 1.2.9 - text-dot-case: 1.2.9 - text-header-case: 1.2.9 - text-is-lower-case: 1.2.9 - text-is-upper-case: 1.2.9 - text-kebab-case: 1.2.9 - text-lower-case: 1.2.9 - text-lower-case-first: 1.2.9 - text-no-case: 1.2.9 - text-param-case: 1.2.9 - text-pascal-case: 1.2.9 - text-path-case: 1.2.9 - text-sentence-case: 1.2.9 - text-snake-case: 1.2.9 - text-swap-case: 1.2.9 - text-title-case: 1.2.9 - text-upper-case: 1.2.9 - text-upper-case-first: 1.2.9 + text-camel-case: 1.2.10 + text-capital-case: 1.2.10 + text-constant-case: 1.2.10 + text-dot-case: 1.2.10 + text-header-case: 1.2.10 + text-is-lower-case: 1.2.10 + text-is-upper-case: 1.2.10 + text-kebab-case: 1.2.10 + text-lower-case: 1.2.10 + text-lower-case-first: 1.2.10 + text-no-case: 1.2.10 + text-param-case: 1.2.10 + text-pascal-case: 1.2.10 + text-path-case: 1.2.10 + text-sentence-case: 1.2.10 + text-snake-case: 1.2.10 + text-swap-case: 1.2.10 + text-title-case: 1.2.10 + text-upper-case: 1.2.10 + text-upper-case-first: 1.2.10 - text-constant-case@1.2.9: + text-constant-case@1.2.10: dependencies: - text-no-case: 1.2.9 - text-upper-case: 1.2.9 + text-no-case: 1.2.10 + text-upper-case: 1.2.10 - text-dot-case@1.2.9: + text-dot-case@1.2.10: dependencies: - text-no-case: 1.2.9 + text-no-case: 1.2.10 - text-header-case@1.2.9: + text-header-case@1.2.10: dependencies: - text-capital-case: 1.2.9 + text-capital-case: 1.2.10 - text-is-lower-case@1.2.9: {} + text-is-lower-case@1.2.10: {} - text-is-upper-case@1.2.9: {} + text-is-upper-case@1.2.10: {} - text-kebab-case@1.2.9: + text-kebab-case@1.2.10: dependencies: - text-no-case: 1.2.9 + text-no-case: 1.2.10 - text-lower-case-first@1.2.9: {} + text-lower-case-first@1.2.10: {} - text-lower-case@1.2.9: {} + text-lower-case@1.2.10: {} - text-no-case@1.2.9: + text-no-case@1.2.10: dependencies: - text-lower-case: 1.2.9 + text-lower-case: 1.2.10 - text-param-case@1.2.9: + text-param-case@1.2.10: dependencies: - text-dot-case: 1.2.9 + text-dot-case: 1.2.10 - text-pascal-case@1.2.9: + text-pascal-case@1.2.10: dependencies: - text-no-case: 1.2.9 + text-no-case: 1.2.10 - text-path-case@1.2.9: + text-path-case@1.2.10: dependencies: - text-dot-case: 1.2.9 + text-dot-case: 1.2.10 - text-sentence-case@1.2.9: + text-sentence-case@1.2.10: dependencies: - text-no-case: 1.2.9 - text-upper-case-first: 1.2.9 + text-no-case: 1.2.10 + text-upper-case-first: 1.2.10 - text-snake-case@1.2.9: + text-snake-case@1.2.10: dependencies: - text-dot-case: 1.2.9 + text-dot-case: 1.2.10 - text-swap-case@1.2.9: {} + text-swap-case@1.2.10: {} - text-title-case@1.2.9: + text-title-case@1.2.10: dependencies: - text-no-case: 1.2.9 - text-upper-case-first: 1.2.9 + text-no-case: 1.2.10 + text-upper-case-first: 1.2.10 - text-upper-case-first@1.2.9: {} + text-upper-case-first@1.2.10: {} - text-upper-case@1.2.9: {} + text-upper-case@1.2.10: {} tiny-invariant@1.3.3: {} @@ -5582,24 +5582,24 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)): + vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)): dependencies: ansi-colors: 4.1.3 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) optionalDependencies: sharp: 0.34.5 - vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0): + vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.55.1 + rollup: 4.55.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 fsevents: 2.3.3 sass: 1.97.2 tsx: 4.21.0 From febd27fe122c3db9e592c713598629179be008c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 20 Jan 2026 09:10:42 +0100 Subject: [PATCH 48/52] add indexes --- .../20260119121935_[2.0.0]_vpn_client_sessions.up.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/migrations/20260119121935_[2.0.0]_vpn_client_sessions.up.sql b/migrations/20260119121935_[2.0.0]_vpn_client_sessions.up.sql index 98b96535f3..90ddb3d0fc 100644 --- a/migrations/20260119121935_[2.0.0]_vpn_client_sessions.up.sql +++ b/migrations/20260119121935_[2.0.0]_vpn_client_sessions.up.sql @@ -18,6 +18,12 @@ CREATE TABLE vpn_client_session ( FOREIGN KEY(user_id) REFERENCES "user"(id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES device(id) ON DELETE CASCADE ); +CREATE INDEX idx_vpn_client_session_user_id ON vpn_client_session(user_id); +CREATE INDEX idx_vpn_client_session_device_id ON vpn_client_session(device_id); +CREATE INDEX idx_vpn_client_session_location_id ON vpn_client_session(location_id); +CREATE INDEX idx_vpn_client_session_state ON vpn_client_session(state); +CREATE INDEX idx_vpn_client_session_created_at ON vpn_client_session(created_at DESC); +CREATE INDEX idx_vpn_client_session_connected_at ON vpn_client_session(connected_at DESC); CREATE TABLE vpn_session_stats ( id bigserial PRIMARY KEY, @@ -33,3 +39,8 @@ CREATE TABLE vpn_session_stats ( FOREIGN KEY (session_id) REFERENCES vpn_client_session(id) ON DELETE CASCADE, FOREIGN KEY (gateway_id) REFERENCES gateway(id) ON DELETE CASCADE ); +CREATE INDEX idx_vpn_session_stats_session_id ON vpn_session_stats(session_id); +CREATE INDEX idx_vpn_session_stats_gateway_id ON vpn_session_stats(gateway_id); +CREATE INDEX idx_vpn_session_stats_collected_at ON vpn_session_stats(collected_at DESC); +CREATE INDEX idx_vpn_session_stats_latest_handshake ON vpn_session_stats(latest_handshake DESC); +CREATE INDEX idx_vpn_session_stats_session_collected ON vpn_session_stats(session_id, collected_at DESC); From 01349f066a5292d6bf1fc55f695d2a00aab9a319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 20 Jan 2026 09:27:54 +0100 Subject: [PATCH 49/52] Revert "update dependencies" This reverts commit 7631e45ceeadf5859b9f13e31b2ded15ba94aa9f. --- Cargo.lock | 78 ++--- flake.lock | 12 +- web/package.json | 20 +- web/pnpm-lock.yaml | 800 ++++++++++++++++++++++----------------------- 4 files changed, 455 insertions(+), 455 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9797f0ff9..41004bb79d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,7 +217,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", ] @@ -459,7 +459,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.18", + "thiserror 2.0.17", ] [[package]] @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.53" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "jobserver", @@ -1150,7 +1150,7 @@ dependencies = [ "rustls-pki-types", "serde", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "x509-parser 0.18.0", ] @@ -1181,7 +1181,7 @@ dependencies = [ "serde_cbor_2", "sqlx", "struct-patch", - "thiserror 2.0.18", + "thiserror 2.0.17", "tonic", "totp-lite", "tracing", @@ -1246,7 +1246,7 @@ dependencies = [ "strum", "strum_macros", "tera", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -1280,7 +1280,7 @@ dependencies = [ "defguard_core", "serde_json", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1292,7 +1292,7 @@ dependencies = [ "defguard_core", "defguard_event_logger", "defguard_mail", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1311,7 +1311,7 @@ dependencies = [ "serde_json", "sqlx", "tera", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1345,7 +1345,7 @@ dependencies = [ "secrecy", "semver", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tokio-stream", "tonic", @@ -1359,7 +1359,7 @@ dependencies = [ "chrono", "defguard_common", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1373,7 +1373,7 @@ dependencies = [ "os_info", "semver", "serde", - "thiserror 2.0.18", + "thiserror 2.0.17", "tonic", "tower", "tracing", @@ -1799,9 +1799,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fixedbitset" @@ -2691,7 +2691,7 @@ dependencies = [ "num-bigint", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.17", "yasna", "zeroize", ] @@ -2768,7 +2768,7 @@ dependencies = [ "native-tls", "nom 7.1.3", "percent-encoding", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tokio-native-tls", "tokio-stream", @@ -4131,7 +4131,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -4152,7 +4152,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -4260,9 +4260,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.7" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" dependencies = [ "pem", "ring", @@ -4554,9 +4554,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc" dependencies = [ "web-time", "zeroize", @@ -4564,9 +4564,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -4818,7 +4818,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.18", + "thiserror 2.0.17", ] [[package]] @@ -5003,7 +5003,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", ] @@ -5131,7 +5131,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -5215,7 +5215,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -5255,7 +5255,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -5281,7 +5281,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.18", + "thiserror 2.0.17", "tracing", "url", "uuid", @@ -5551,11 +5551,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.17", ] [[package]] @@ -5571,9 +5571,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -6908,7 +6908,7 @@ dependencies = [ "oid-registry 0.8.1", "ring", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", ] @@ -7061,9 +7061,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" [[package]] name = "zopfli" diff --git a/flake.lock b/flake.lock index 023f36f5b1..1bd30e0f57 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768564909, - "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "lastModified": 1768127708, + "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1768877311, - "narHash": "sha256-abSDl0cNr0B+YCsIDpO1SjXD9JMxE4s8EFnhLEFVovI=", + "lastModified": 1768359079, + "narHash": "sha256-a016mOfKconYrYo3fZLN6c2cnmqYYd44g2bUrBZAsQc=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "59e4ab96304585fde3890025fd59bd2717985cc1", + "rev": "0357d1826057686637e41147545402cbbda420ce", "type": "github" }, "original": { diff --git a/web/package.json b/web/package.json index b127994a73..02380890a8 100644 --- a/web/package.json +++ b/web/package.json @@ -15,17 +15,17 @@ "dependencies": { "@axa-ch/react-polymorphic-types": "^1.4.1", "@floating-ui/react": "^0.27.16", - "@inlang/paraglide-js": "^2.9.0", + "@inlang/paraglide-js": "^2.8.0", "@react-hook/resize-observer": "^2.0.2", "@shortercode/webzip": "1.1.1-0", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", "@tanstack/react-devtools": "^0.9.2", "@tanstack/react-form": "^1.27.7", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.90.17", "@tanstack/react-query-devtools": "^5.91.2", - "@tanstack/react-router": "^1.153.2", - "@tanstack/react-router-devtools": "^1.153.2", + "@tanstack/react-router": "^1.149.3", + "@tanstack/react-router-devtools": "^1.149.3", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.18", "@uidotdev/usehooks": "^2.4.1", @@ -37,18 +37,18 @@ "humanize-duration": "^3.33.2", "ipaddr.js": "^2.3.0", "lodash-es": "^4.17.22", - "motion": "^12.27.1", + "motion": "^12.26.2", "qrcode.react": "^4.2.0", "qs": "^6.14.1", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-intersection-observer": "^10.0.2", + "react-intersection-observer": "^10.0.0", "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", "recharts": "^3.6.0", "rehype-raw": "^7.0.0", "rxjs": "^7.8.2", - "text-case": "^1.2.10", + "text-case": "^1.2.9", "zod": "^4.3.5", "zustand": "^5.0.10" }, @@ -56,18 +56,18 @@ "@biomejs/biome": "2.3.11", "@inlang/paraglide-js": "2.8.0", "@tanstack/devtools-vite": "^0.4.1", - "@tanstack/router-plugin": "^1.153.2", + "@tanstack/router-plugin": "^1.149.3", "@types/byte-size": "^8.1.2", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", - "@types/node": "^25.0.9", + "@types/node": "^25.0.8", "@types/qs": "^6.14.0", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.2", "autoprefixer": "^10.4.23", "globals": "^17.0.0", - "prettier": "^3.8.0", + "prettier": "^3.7.4", "sass": "^1.97.2", "sharp": "^0.34.5", "stylelint": "^16.26.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 2d5ced3e86..788f93983a 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^0.27.16 version: 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@inlang/paraglide-js': - specifier: ^2.9.0 - version: 2.9.0 + specifier: ^2.8.0 + version: 2.8.0 '@react-hook/resize-observer': specifier: ^2.0.2 version: 2.0.2(react@19.2.3) @@ -36,17 +36,17 @@ importers: specifier: ^1.27.7 version: 1.27.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-query': - specifier: ^5.90.19 - version: 5.90.19(react@19.2.3) + specifier: ^5.90.17 + version: 5.90.17(react@19.2.3) '@tanstack/react-query-devtools': specifier: ^5.91.2 - version: 5.91.2(@tanstack/react-query@5.90.19(react@19.2.3))(react@19.2.3) + version: 5.91.2(@tanstack/react-query@5.90.17(react@19.2.3))(react@19.2.3) '@tanstack/react-router': - specifier: ^1.153.2 - version: 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.149.3 + version: 1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': - specifier: ^1.153.2 - version: 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.153.2)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.149.3 + version: 1.149.3(@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.149.3)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -81,8 +81,8 @@ importers: specifier: ^4.17.22 version: 4.17.22 motion: - specifier: ^12.27.1 - version: 12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^12.26.2 + version: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) qrcode.react: specifier: ^4.2.0 version: 4.2.0(react@19.2.3) @@ -96,8 +96,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(react@19.2.3) react-intersection-observer: - specifier: ^10.0.2 - version: 10.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^10.0.0 + version: 10.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-loading-skeleton: specifier: ^3.5.0 version: 3.5.0(react@19.2.3) @@ -114,8 +114,8 @@ importers: specifier: ^7.8.2 version: 7.8.2 text-case: - specifier: ^1.2.10 - version: 1.2.10 + specifier: ^1.2.9 + version: 1.2.9 zod: specifier: ^4.3.5 version: 4.3.5 @@ -128,10 +128,10 @@ importers: version: 2.3.11 '@tanstack/devtools-vite': specifier: ^0.4.1 - version: 0.4.1(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)) + version: 0.4.1(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)) '@tanstack/router-plugin': - specifier: ^1.153.2 - version: 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)) + specifier: ^1.149.3 + version: 1.149.3(@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -142,8 +142,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^25.0.9 - version: 25.0.9 + specifier: ^25.0.8 + version: 25.0.8 '@types/qs': specifier: ^6.14.0 version: 6.14.0 @@ -155,7 +155,7 @@ importers: version: 19.2.3(@types/react@19.2.8) '@vitejs/plugin-react-swc': specifier: ^4.2.2 - version: 4.2.2(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)) + version: 4.2.2(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)) autoprefixer: specifier: ^10.4.23 version: 10.4.23(postcss@8.5.6) @@ -163,8 +163,8 @@ importers: specifier: ^17.0.0 version: 17.0.0 prettier: - specifier: ^3.8.0 - version: 3.8.0 + specifier: ^3.7.4 + version: 3.7.4 sass: specifier: ^1.97.2 version: 1.97.2 @@ -185,10 +185,10 @@ importers: version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) + version: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) vite-plugin-image-optimizer: specifier: ^2.0.3 - version: 2.0.3(sharp@0.34.5)(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)) + version: 2.0.3(sharp@0.34.5)(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)) packages: @@ -687,8 +687,8 @@ packages: cpu: [x64] os: [win32] - '@inlang/paraglide-js@2.9.0': - resolution: {integrity: sha512-SDtemGiHuqwtBwprs3UEjb2K2HpAcG0km6nvmbVtd5SVwmgny2IqUyLynGBJ0F2s5Ntv2/cSeBLB59qXHynrVw==} + '@inlang/paraglide-js@2.8.0': + resolution: {integrity: sha512-ataaSmV53zz+tIr+KJLdC3tTB1uikS79hvtLlZk2ikbGRB/kcyQeg+lsqzjsXCAvy0/O28ucCRjxbHsTzOVQVg==} hasBin: true '@inlang/recommend-sherlock@0.2.1': @@ -853,128 +853,128 @@ packages: '@rolldown/pluginutils@1.0.0-beta.47': resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} - '@rollup/rollup-android-arm-eabi@4.55.2': - resolution: {integrity: sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==} + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.55.2': - resolution: {integrity: sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==} + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.55.2': - resolution: {integrity: sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==} + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.55.2': - resolution: {integrity: sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==} + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.55.2': - resolution: {integrity: sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==} + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.55.2': - resolution: {integrity: sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==} + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.55.2': - resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==} + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.55.2': - resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==} + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.55.2': - resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==} + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.55.2': - resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==} + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.55.2': - resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==} + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loong64-musl@4.55.2': - resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==} + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.55.2': - resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==} + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-ppc64-musl@4.55.2': - resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==} + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.55.2': - resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==} + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.55.2': - resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==} + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.55.2': - resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==} + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.55.2': - resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==} + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.55.2': - resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==} + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] - '@rollup/rollup-openbsd-x64@4.55.2': - resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==} + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.55.2': - resolution: {integrity: sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==} + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.55.2': - resolution: {integrity: sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==} + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.55.2': - resolution: {integrity: sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==} + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.55.2': - resolution: {integrity: sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==} + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.55.2': - resolution: {integrity: sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==} + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} cpu: [x64] os: [win32] @@ -1048,68 +1048,68 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@swc/core-darwin-arm64@1.15.10': - resolution: {integrity: sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA==} + '@swc/core-darwin-arm64@1.15.8': + resolution: {integrity: sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.10': - resolution: {integrity: sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ==} + '@swc/core-darwin-x64@1.15.8': + resolution: {integrity: sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.10': - resolution: {integrity: sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg==} + '@swc/core-linux-arm-gnueabihf@1.15.8': + resolution: {integrity: sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.10': - resolution: {integrity: sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ==} + '@swc/core-linux-arm64-gnu@1.15.8': + resolution: {integrity: sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.15.10': - resolution: {integrity: sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw==} + '@swc/core-linux-arm64-musl@1.15.8': + resolution: {integrity: sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.15.10': - resolution: {integrity: sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg==} + '@swc/core-linux-x64-gnu@1.15.8': + resolution: {integrity: sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.15.10': - resolution: {integrity: sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw==} + '@swc/core-linux-x64-musl@1.15.8': + resolution: {integrity: sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.15.10': - resolution: {integrity: sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw==} + '@swc/core-win32-arm64-msvc@1.15.8': + resolution: {integrity: sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.10': - resolution: {integrity: sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw==} + '@swc/core-win32-ia32-msvc@1.15.8': + resolution: {integrity: sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.10': - resolution: {integrity: sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA==} + '@swc/core-win32-x64-msvc@1.15.8': + resolution: {integrity: sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.10': - resolution: {integrity: sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==} + '@swc/core@1.15.8': + resolution: {integrity: sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1156,16 +1156,16 @@ packages: '@tanstack/form-core@1.27.7': resolution: {integrity: sha512-nvogpyE98fhb0NDw1Bf2YaCH+L7ZIUgEpqO9TkHucDn6zg3ni521boUpv0i8HKIrmmFwDYjWZoCnrgY4HYWTkw==} - '@tanstack/history@1.153.2': - resolution: {integrity: sha512-TVa0Wju5w6JZGq/S74Q7TQNtKXDatJaB4NYrhMZVU9ETlkgpr35NhDfOzsCJ93P0KCo1ZoDodlFp3c54/dLsyw==} + '@tanstack/history@1.145.7': + resolution: {integrity: sha512-gMo/ReTUp0a3IOcZoI3hH6PLDC2R/5ELQ7P2yu9F6aEkA0wSQh+Q4qzMrtcKvF2ut0oE+16xWCGDo/TdYd6cEQ==} engines: {node: '>=12'} '@tanstack/pacer-lite@0.1.1': resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.90.19': - resolution: {integrity: sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA==} + '@tanstack/query-core@5.90.17': + resolution: {integrity: sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ==} '@tanstack/query-devtools@5.92.0': resolution: {integrity: sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==} @@ -1194,25 +1194,25 @@ packages: '@tanstack/react-query': ^5.90.14 react: ^18 || ^19 - '@tanstack/react-query@5.90.19': - resolution: {integrity: sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ==} + '@tanstack/react-query@5.90.17': + resolution: {integrity: sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.153.2': - resolution: {integrity: sha512-LCEuRIyrF0tNKCBspR+TQj13MQ7sTCE4QkkuKAOp30nSdWLxq53bltnGs9bj/V/PTD52JibuAOYyxB94ssWZUA==} + '@tanstack/react-router-devtools@1.149.3': + resolution: {integrity: sha512-QH16WA0NkfZSxku8fHy0CFm42MJ1mXeDnCAsaIZXKypv935MQzsXEvwn6ZZDkH8qP8eCQBoYlRVZmWiIr+9Omw==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.153.2 - '@tanstack/router-core': ^1.153.2 + '@tanstack/react-router': ^1.149.3 + '@tanstack/router-core': ^1.149.3 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.153.2': - resolution: {integrity: sha512-fAXUBA2gZAId7h2eSHsRcgTeF8pioUz8V5rrQ+IrvA0a6IsxhbTSKLYyqUg4jRDkkcUKtM8StKtvbZCY+0IYWw==} + '@tanstack/react-router@1.149.3': + resolution: {integrity: sha512-yklZ2LSXLGfhW4PXu2N98yhGk8qtlkUbFRV42np0rx46s50wB5sXRkjdnqyGuDG/dldaBIi76M6vWg84Pmb4+A==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1237,30 +1237,30 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.153.2': - resolution: {integrity: sha512-WLaR+rSNW7bj9UCJQ3SKpuh6nZBZkpGnf2mpjn/uRB6joIQ3BU7aRdhb7w9Via/MP52iaHh5sd8NY3MaLpF2tQ==} + '@tanstack/router-core@1.149.3': + resolution: {integrity: sha512-obXmQ2hElxqjQ9cpABjXOvR/aQG+uG9ALEcVvyqP1ae57Fb3VhOuynmc2k/eVgx/bKKvxe2cqj4wCG04O0i5Zg==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.153.2': - resolution: {integrity: sha512-53gFlnz2oUeGvRwu7hzi+jlqm5F5X1XwNniirCTjggsV5P+FVQ7YJ+gfMuN5MHonWmVCLd1QqGkl2nYRTGHeTg==} + '@tanstack/router-devtools-core@1.149.3': + resolution: {integrity: sha512-hgGPqqs/yD2XgmyTdmwBH6FrXnMbcsNWLup7nHPp/NGod9mtGKqSR2gBpicjZTBpaX/ihX29GG1s0l5MKmpQXA==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.153.2 + '@tanstack/router-core': ^1.149.3 csstype: ^3.0.10 peerDependenciesMeta: csstype: optional: true - '@tanstack/router-generator@1.153.2': - resolution: {integrity: sha512-bEhmCtXq5vv3HukKq5zmTDBNDRqVllYxsHoWtqEvHv5hCb5xwKKfUMGemRoiQ96/wLFuGnA5DYkem2GZWcG3wg==} + '@tanstack/router-generator@1.149.3': + resolution: {integrity: sha512-SKjsYiXT81wO2V1wF+7ITc/EDvt1xsN4JO5MvFYFL1s5Pq1gCW7/nI4yvqZzGm7aE26J8TLdTn419GfuE9SIlw==} engines: {node: '>=12'} - '@tanstack/router-plugin@1.153.2': - resolution: {integrity: sha512-aMMc70ChM0wBYOToq39kTMKI2A0EKWpumiKTJyAwEglXf0raF48+26Fmv0gr9/5CLvD0g8ljllsskVDyzg8oDw==} + '@tanstack/router-plugin@1.149.3': + resolution: {integrity: sha512-OO+S5czNWGC/+IC6XtKwPkqkRRaVJna6P8jNu+trV1hByhl1NKvPFRFBIqvUlMsM100hAesb0Jk2LsKwleywNA==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.153.2 + '@tanstack/react-router': ^1.149.3 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' vite-plugin-solid: ^2.11.10 webpack: '>=5.92.0' @@ -1354,8 +1354,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.0.9': - resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} + '@types/node@25.0.8': + resolution: {integrity: sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==} '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1461,8 +1461,8 @@ packages: balanced-match@2.0.0: resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} - baseline-browser-mapping@2.9.15: - resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} + baseline-browser-mapping@2.9.14: + resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true binary-extensions@2.3.0: @@ -1487,8 +1487,8 @@ packages: '@75lb/nature': optional: true - cacheable@2.3.2: - resolution: {integrity: sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==} + cacheable@2.3.1: + resolution: {integrity: sha512-yr+FSHWn1ZUou5LkULX/S+jhfgfnLbuKQjE40tyEd4fxGZVMbBL5ifno0J0OauykS8UiCSgHi+DV/YD+rjFxFg==} call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} @@ -1502,8 +1502,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001765: - resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==} + caniuse-lite@1.0.30001764: + resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1611,8 +1611,8 @@ packages: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + d3-format@3.1.1: + resolution: {integrity: sha512-ryitBnaRbXQtgZ/gU50GSn6jQRwinSCQclpakXymvLd8ytTgE5bmSfgYcUxD7XYL34qHhFDyVk71qqKsfSyvmA==} engines: {node: '>=12'} d3-interpolate@3.0.1: @@ -1658,8 +1658,8 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - decode-named-character-reference@1.3.0: - resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} @@ -1732,8 +1732,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-toolkit@1.44.0: - resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} + es-toolkit@1.43.0: + resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} @@ -1752,8 +1752,8 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1784,15 +1784,15 @@ packages: picomatch: optional: true - file-entry-cache@11.1.2: - resolution: {integrity: sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==} + file-entry-cache@11.1.1: + resolution: {integrity: sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - flat-cache@6.1.20: - resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} + flat-cache@6.1.19: + resolution: {integrity: sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==} flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -1813,8 +1813,8 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.27.1: - resolution: {integrity: sha512-cEAqO69kcZt3gL0TGua8WTgRQfv4J57nqt1zxHtLKwYhAwA0x9kDS/JbMa1hJbwkGY74AGJKvZ9pX/IqWZtZWQ==} + framer-motion@12.26.2: + resolution: {integrity: sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2025,8 +2025,8 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} - isbot@5.1.33: - resolution: {integrity: sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg==} + isbot@5.1.32: + resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==} engines: {node: '>=18'} isexe@2.0.0: @@ -2210,14 +2210,14 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - motion-dom@12.27.1: - resolution: {integrity: sha512-V/53DA2nBqKl9O2PMJleSUb/G0dsMMeZplZwgIQf5+X0bxIu7Q1cTv6DrjvTTGYRm3+7Y5wMlRZ1wT61boU/bQ==} + motion-dom@12.26.2: + resolution: {integrity: sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==} motion-utils@12.24.10: resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} - motion@12.27.1: - resolution: {integrity: sha512-FAZTPDm1LccBdWSL46WLnEdTSHmdVx+fdWK8f61qBQn67MmFefXLXlrwy94rK2DDsd9A50Gj8H+LYCgQ/cQlFg==} + motion@12.26.2: + resolution: {integrity: sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2313,8 +2313,8 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.8.0: - resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} hasBin: true @@ -2324,8 +2324,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - qified@0.6.0: - resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} + qified@0.5.3: + resolution: {integrity: sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==} engines: {node: '>=20'} qrcode.react@4.2.0: @@ -2345,8 +2345,8 @@ packages: peerDependencies: react: ^19.2.3 - react-intersection-observer@10.0.2: - resolution: {integrity: sha512-lAMzxVWrBko6SLd1jx6l84fVrzJu91hpxHlvD2as2Wec9mDCjdYXwc5xNOFBchpeBir0Y7AGBW+C/AYMa7CSFg==} + react-intersection-observer@10.0.0: + resolution: {integrity: sha512-JJRgcnFQoVXmbE5+GXr1OS1NDD1gHk0HyfpLcRf0575IbJz+io8yzs4mWVlfaqOQq1FiVjLvuYAdEEcrrCfveg==} peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2443,8 +2443,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.55.2: - resolution: {integrity: sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==} + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2628,68 +2628,68 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - text-camel-case@1.2.10: - resolution: {integrity: sha512-KNrWeZzQT+gh73V1LnmgTkjK7V+tMRjLCc6VrGwkqbiRdnGVIWBUgIvVnvnaVCxIvZ/2Ke8DCmgPirlQcCqD3Q==} + text-camel-case@1.2.9: + resolution: {integrity: sha512-wKYs9SgRxYizJE1mneR7BbLNlGw2IYzJAS8XwkWIry0CTbO1gvvPkFsx5Z1/hr+VqUaBqx9q3yKd30HpZLdMsQ==} - text-capital-case@1.2.10: - resolution: {integrity: sha512-yvViUJKSSQcRO58je224bhPHg/Hij9MEY43zuKShtFzrPwW/fOAarUJ5UkTMSB81AOO1m8q+JiFdxMF4etKZbA==} + text-capital-case@1.2.9: + resolution: {integrity: sha512-X5zV8U8pxtq2xS2t46lgAWqZdDbgWMKq03MQSNwY2CJdQCsdTNh144E2Q/q9wBxWzSBUXn+jRc9kF+Gs8/pGhA==} - text-case@1.2.10: - resolution: {integrity: sha512-5bY3Ks/u7OJ5YO69iyXrG5Xf2wUZeyko7U78nPUnYoSeuNeAfA5uAix5hTspfkl6smm3yCBObrex+kFvzeIcJg==} + text-case@1.2.9: + resolution: {integrity: sha512-zZVdA8rMcjx9zhekdUuOPZShc25UTV7W8/ddKbgbPtfCEvIiToPtWiSd2lXLSuiGMovNhJ4+Tw49xll9o9ts+Q==} - text-constant-case@1.2.10: - resolution: {integrity: sha512-/OfU798O2wrwKN9kQf71WhJeAlklGnbby0Tupp+Ez9NXymW+6oF9LWDRTkN+OreTmHucdvp4WQd6O5Rah5zj8A==} + text-constant-case@1.2.9: + resolution: {integrity: sha512-Vosm6nC7Gag+JFakJHwqS9AXRNgl07j5KZ7srU9cYuKRzYwrxzeJ4RpEogRBNHw7CfmOm0j5FGEznblWtu7pIw==} - text-dot-case@1.2.10: - resolution: {integrity: sha512-vf4xguy5y6e39RlDZeWZFMDf2mNkR23VTSVb9e68dUSpfJscG9/1YWWpW3n8TinzQxBZlsn5sT5olL33MvvQXw==} + text-dot-case@1.2.9: + resolution: {integrity: sha512-N83hsnvGdSO9q9AfNSB9Cy1LFDNN2MCx53LcxtaPoDWPUTk47fv0JlvIY1tgY0wyzCiThF03kVj3jworvAOScA==} - text-header-case@1.2.10: - resolution: {integrity: sha512-sVb1NY9bwxtu+Z7CVyWbr+I0AkWtF0kEHL/Zz5V2u/WdkjK5tKBwl5nXf0NGy9da4ZUYTBb+TmQpOIqihzvFMQ==} + text-header-case@1.2.9: + resolution: {integrity: sha512-TqryEKcYisQAfWLbtT3xPnZlMZ/mySO1uS+LUg+B0eNuqgETrSzVpXIUj5E6Zf/EyJHgpZf4VndbAXtOMJuT4w==} - text-is-lower-case@1.2.10: - resolution: {integrity: sha512-dMTeTgrdWWfYf3fKxvjMkDPuXWv96cWbd1Uym6Zjv9H855S1uHxjkFsGbTYJ2tEK0NvAylRySTQlI6axlcMc4w==} + text-is-lower-case@1.2.9: + resolution: {integrity: sha512-cEurrWSnYVYqL8FSwl5cK4mdfqF7qNDCcKJgXI3NnfTesiB8umxAhdlQoErrRYI1xEvYr2WN0MI333EehUhQjg==} - text-is-upper-case@1.2.10: - resolution: {integrity: sha512-PGD/cXoXECGAY1HVZxDdmpJUW2ZUAKQ6DTamDfCHC9fc/z4epOz0pB/ThBnjJA3fz+d2ApkMjAfZDjuZFcodzg==} + text-is-upper-case@1.2.9: + resolution: {integrity: sha512-HxsWr3VCsXXiLlhD0c+Ey+mS2lOTCiSJbkepjaXNHl2bp33KiscQaiG0qLwQmmpZQm4SJCg2s9FkndxS0RNDLQ==} - text-kebab-case@1.2.10: - resolution: {integrity: sha512-3XZJAApx5JQpUO7eXo7GQ2TyRcGw3OVbqxz6QJb2h+N8PbLLbz3zJVeXdGrhTkoUIbkSZ6PmHx6LRDaHXTdMcA==} + text-kebab-case@1.2.9: + resolution: {integrity: sha512-nOUyNR5Ej2B9D/wyyXfwUEv26+pQuOb1pEX+ojE37mCIWo8QeOxw5y6nxuqDmG7NrEPzbO6265UMV+EICH13Cw==} - text-lower-case-first@1.2.10: - resolution: {integrity: sha512-Oro84jZPDLD9alfdZWmtFHYTvCaaSz2o4thPtjMsK4GAkTyVg9juYXWj0y0YFyjLYGH69muWsBe4/MR5S7iolw==} + text-lower-case-first@1.2.9: + resolution: {integrity: sha512-iiphHTV7PVH0MljrEQUA9iBE7jfDpXoi4RQju3WzZU3BRVbS6540cNZgxR19hWa0z6z/7cJTH0Ls9LPBaiUfKg==} - text-lower-case@1.2.10: - resolution: {integrity: sha512-c9j5pIAN3ObAp1+4R7970e1bgtahTRF/5ZQdX2aJBuBngYTYZZIck0NwFXUKk5BnYpLGsre5KFHvpqvf4IYKgg==} + text-lower-case@1.2.9: + resolution: {integrity: sha512-53AOnDrhPpiAUQkgY1SHleKUXp/u7GsqRX13NcCREZscmtjLLJ099uxMRjkK7q2KwHkFYVPl9ytkQlTkTQLS0w==} - text-no-case@1.2.10: - resolution: {integrity: sha512-4/m79pzQrywrwEG5lCULY1lQvFY+EKjhH9xSMT6caPK5plqzm9Y7rXyv+UXPd3s9qH6QODZnvsAYWW3M0JgxRA==} + text-no-case@1.2.9: + resolution: {integrity: sha512-IcCt328KaapimSrytP4ThfC8URmHZb2DgOqCL9BYvGjpxY2lDiqCkIQk9sClZtwcELs2gTnq83a7jNc573FTLA==} - text-param-case@1.2.10: - resolution: {integrity: sha512-hkavcLsRRzZcGryPAshct1AwIOMj/FexYjMaLpGZCYYBn1lcZEeyMzJZPSckzkOYpq35LYSQr3xZto9XU5OAsw==} + text-param-case@1.2.9: + resolution: {integrity: sha512-nR/Ju9amY3aQS1en2CUCgqN/ZiZIVdDyjlJ3xX5J92ChBevGuA4o9K10fh3JGMkbzK97Vcb+bWQJ4Q+Svz+GyQ==} - text-pascal-case@1.2.10: - resolution: {integrity: sha512-/kynZD8vTYOmm/RECjIDaz3qYEUZc/N/bnC79XuAFxwXjdNVjj/jGovKJLRzqsYK/39N22XpGcVmGg7yIrbk6w==} + text-pascal-case@1.2.9: + resolution: {integrity: sha512-o6ZxMGjWDTUW54pcghpXes+C2PqbYRMdU5mHrIhueb6z6nq1NueiIOeCUdrSjN/3wXfhCmnFjK7/d9aRGZNqSg==} - text-path-case@1.2.10: - resolution: {integrity: sha512-vbKdRCaVEeOaW6sm24QP9NbH7TS9S4ZQ3u19H8eylDox7m2HtFwYIBjAPv+v3z4I/+VjrMy9LB54lNP1uEqRHw==} + text-path-case@1.2.9: + resolution: {integrity: sha512-s8cJ6r5TkJp5ticXMgtxd7f12odEN4d1CfX5u4aoz6jcUtBR2lDqzIhVimkqWFMJ4UKPSrmilUha8Xc2BPi+ow==} - text-sentence-case@1.2.10: - resolution: {integrity: sha512-NO4MRlbfxFhl9QgQLuCL4xHmvE7PUWHVPWsZxQ5nzRtDjXOUllWvtsvl8CP5tBEvBmzg0kwfflxfhRtr5vBQGg==} + text-sentence-case@1.2.9: + resolution: {integrity: sha512-/G/Yi5kZfUa1edFRV4O3lGZAkbDZTFvlwW8CYfH7szkEGe2k2MYEYbOyAkGRVQEGV6V6JiuUAaP3VS9c1tB6nQ==} - text-snake-case@1.2.10: - resolution: {integrity: sha512-6ttMZ+B9jkHKun908HYr4xSvEtlbfJJ4MvpQ06JEKRGhwjMI0x8t2Wywp+MEzN6142O6E/zKhra18KyBL6cvXA==} + text-snake-case@1.2.9: + resolution: {integrity: sha512-+ZrqK19ynF/TLQZ7ynqVrL2Dy04uu9syYZwsm8PhzUdsY3XrwPy6QiRqhIEFqhyWbShPcfyfmheer5UEQqFxlw==} - text-swap-case@1.2.10: - resolution: {integrity: sha512-vO3jwInIk0N77oEFakYZ2Hn/llTmRwf2c3RvkX/LfvmLWVp+3QcIc6bwUEtbqGQ5Xh2okjFhYrfkHZstVc3N4Q==} + text-swap-case@1.2.9: + resolution: {integrity: sha512-g5fp12ldktYKK9wdHRMvvtSCQrZYNv/D+ZGLumDsvAY4q9T5bCMO2IWMkIP1F5gVQrysdHH6Xv877P/pjUq1iw==} - text-title-case@1.2.10: - resolution: {integrity: sha512-bqA+WWexUMWu9A3fdNar+3GXXW+c5xOvMyuK5hOx/w0AlqhyQptyCrMFjGB8Fd9dxbryBNmJ+5rWtC1OBDxlaA==} + text-title-case@1.2.9: + resolution: {integrity: sha512-RAtC9cdmPp41ns5/HXZBsaQg71BsHT7uZpj2ojTtuFa8o2dNuRYYOrSmy5YdLRIAJQ6WK5hQVpV3jHuq7a+4Tw==} - text-upper-case-first@1.2.10: - resolution: {integrity: sha512-VXs7j7BbpKwvolDh5fwpYRmMrUHGkxbY8E90fhBzKUoKfadvWmPT/jFieoZ4UPLzr208pXvQEFbb2zO9Qzs9Fg==} + text-upper-case-first@1.2.9: + resolution: {integrity: sha512-wEDD1B6XqJmEV+xEnBJd+2sBCHZ+7fvA/8Rv/o8+dAsp05YWjYP/kjB8sPH6zqzW0s6jtehIg4IlcKjcYxk2CQ==} - text-upper-case@1.2.10: - resolution: {integrity: sha512-L1AtZ8R+jtSMTq0Ffma9R4Rzbrc3iuYW89BmWFH41AwnDfRmEBlBOllm1ZivRLQ/6pEu2p+3XKBHx9fsMl2CWg==} + text-upper-case@1.2.9: + resolution: {integrity: sha512-K/0DNT7a4z8eah2spARtoJllTZyrNTo6Uc0ujhN/96Ir9uJ/slpahfs13y46H9osL3daaLl3O7iXOkW4xtX6bg==} tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3284,7 +3284,7 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inlang/paraglide-js@2.9.0': + '@inlang/paraglide-js@2.8.0': dependencies: '@inlang/recommend-sherlock': 0.2.1 '@inlang/sdk': 2.6.0 @@ -3452,79 +3452,79 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rollup/rollup-android-arm-eabi@4.55.2': + '@rollup/rollup-android-arm-eabi@4.55.1': optional: true - '@rollup/rollup-android-arm64@4.55.2': + '@rollup/rollup-android-arm64@4.55.1': optional: true - '@rollup/rollup-darwin-arm64@4.55.2': + '@rollup/rollup-darwin-arm64@4.55.1': optional: true - '@rollup/rollup-darwin-x64@4.55.2': + '@rollup/rollup-darwin-x64@4.55.1': optional: true - '@rollup/rollup-freebsd-arm64@4.55.2': + '@rollup/rollup-freebsd-arm64@4.55.1': optional: true - '@rollup/rollup-freebsd-x64@4.55.2': + '@rollup/rollup-freebsd-x64@4.55.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.55.2': + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.55.2': + '@rollup/rollup-linux-arm-musleabihf@4.55.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.55.2': + '@rollup/rollup-linux-arm64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.55.2': + '@rollup/rollup-linux-arm64-musl@4.55.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.55.2': + '@rollup/rollup-linux-loong64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-loong64-musl@4.55.2': + '@rollup/rollup-linux-loong64-musl@4.55.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.55.2': + '@rollup/rollup-linux-ppc64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-ppc64-musl@4.55.2': + '@rollup/rollup-linux-ppc64-musl@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.55.2': + '@rollup/rollup-linux-riscv64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.55.2': + '@rollup/rollup-linux-riscv64-musl@4.55.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.55.2': + '@rollup/rollup-linux-s390x-gnu@4.55.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.55.2': + '@rollup/rollup-linux-x64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-x64-musl@4.55.2': + '@rollup/rollup-linux-x64-musl@4.55.1': optional: true - '@rollup/rollup-openbsd-x64@4.55.2': + '@rollup/rollup-openbsd-x64@4.55.1': optional: true - '@rollup/rollup-openharmony-arm64@4.55.2': + '@rollup/rollup-openharmony-arm64@4.55.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.55.2': + '@rollup/rollup-win32-arm64-msvc@4.55.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.55.2': + '@rollup/rollup-win32-ia32-msvc@4.55.1': optional: true - '@rollup/rollup-win32-x64-gnu@4.55.2': + '@rollup/rollup-win32-x64-gnu@4.55.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.55.2': + '@rollup/rollup-win32-x64-msvc@4.55.1': optional: true '@shortercode/webzip@1.1.1-0': {} @@ -3598,51 +3598,51 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@swc/core-darwin-arm64@1.15.10': + '@swc/core-darwin-arm64@1.15.8': optional: true - '@swc/core-darwin-x64@1.15.10': + '@swc/core-darwin-x64@1.15.8': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.10': + '@swc/core-linux-arm-gnueabihf@1.15.8': optional: true - '@swc/core-linux-arm64-gnu@1.15.10': + '@swc/core-linux-arm64-gnu@1.15.8': optional: true - '@swc/core-linux-arm64-musl@1.15.10': + '@swc/core-linux-arm64-musl@1.15.8': optional: true - '@swc/core-linux-x64-gnu@1.15.10': + '@swc/core-linux-x64-gnu@1.15.8': optional: true - '@swc/core-linux-x64-musl@1.15.10': + '@swc/core-linux-x64-musl@1.15.8': optional: true - '@swc/core-win32-arm64-msvc@1.15.10': + '@swc/core-win32-arm64-msvc@1.15.8': optional: true - '@swc/core-win32-ia32-msvc@1.15.10': + '@swc/core-win32-ia32-msvc@1.15.8': optional: true - '@swc/core-win32-x64-msvc@1.15.10': + '@swc/core-win32-x64-msvc@1.15.8': optional: true - '@swc/core@1.15.10': + '@swc/core@1.15.8': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.10 - '@swc/core-darwin-x64': 1.15.10 - '@swc/core-linux-arm-gnueabihf': 1.15.10 - '@swc/core-linux-arm64-gnu': 1.15.10 - '@swc/core-linux-arm64-musl': 1.15.10 - '@swc/core-linux-x64-gnu': 1.15.10 - '@swc/core-linux-x64-musl': 1.15.10 - '@swc/core-win32-arm64-msvc': 1.15.10 - '@swc/core-win32-ia32-msvc': 1.15.10 - '@swc/core-win32-x64-msvc': 1.15.10 + '@swc/core-darwin-arm64': 1.15.8 + '@swc/core-darwin-x64': 1.15.8 + '@swc/core-linux-arm-gnueabihf': 1.15.8 + '@swc/core-linux-arm64-gnu': 1.15.8 + '@swc/core-linux-arm64-musl': 1.15.8 + '@swc/core-linux-x64-gnu': 1.15.8 + '@swc/core-linux-x64-musl': 1.15.8 + '@swc/core-win32-arm64-msvc': 1.15.8 + '@swc/core-win32-ia32-msvc': 1.15.8 + '@swc/core-win32-x64-msvc': 1.15.8 '@swc/counter@0.1.3': {} @@ -3671,7 +3671,7 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.4.1(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0))': + '@tanstack/devtools-vite@0.4.1(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.6 '@babel/generator': 7.28.6 @@ -3683,7 +3683,7 @@ snapshots: chalk: 5.6.2 launch-editor: 2.12.0 picomatch: 4.0.3 - vite: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) transitivePeerDependencies: - bufferutil - supports-color @@ -3711,11 +3711,11 @@ snapshots: '@tanstack/pacer-lite': 0.1.1 '@tanstack/store': 0.7.7 - '@tanstack/history@1.153.2': {} + '@tanstack/history@1.145.7': {} '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.90.19': {} + '@tanstack/query-core@5.90.17': {} '@tanstack/query-devtools@5.92.0': {} @@ -3740,34 +3740,34 @@ snapshots: transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.19(react@19.2.3))(react@19.2.3)': + '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.17(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/query-devtools': 5.92.0 - '@tanstack/react-query': 5.90.19(react@19.2.3) + '@tanstack/react-query': 5.90.17(react@19.2.3) react: 19.2.3 - '@tanstack/react-query@5.90.19(react@19.2.3)': + '@tanstack/react-query@5.90.17(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.90.19 + '@tanstack/query-core': 5.90.17 react: 19.2.3 - '@tanstack/react-router-devtools@1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.153.2)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router-devtools@1.149.3(@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.149.3)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-devtools-core': 1.153.2(@tanstack/router-core@1.153.2)(csstype@3.2.3) + '@tanstack/react-router': 1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-devtools-core': 1.149.3(@tanstack/router-core@1.149.3)(csstype@3.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@tanstack/router-core': 1.153.2 + '@tanstack/router-core': 1.149.3 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/history': 1.153.2 + '@tanstack/history': 1.145.7 '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.153.2 - isbot: 5.1.33 + '@tanstack/router-core': 1.149.3 + isbot: 5.1.32 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) tiny-invariant: 1.3.3 @@ -3792,9 +3792,9 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@tanstack/router-core@1.153.2': + '@tanstack/router-core@1.149.3': dependencies: - '@tanstack/history': 1.153.2 + '@tanstack/history': 1.145.7 '@tanstack/store': 0.8.0 cookie-es: 2.0.0 seroval: 1.4.2 @@ -3802,21 +3802,21 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.153.2(@tanstack/router-core@1.153.2)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.149.3(@tanstack/router-core@1.149.3)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.153.2 + '@tanstack/router-core': 1.149.3 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.153.2': + '@tanstack/router-generator@1.149.3': dependencies: - '@tanstack/router-core': 1.153.2 + '@tanstack/router-core': 1.149.3 '@tanstack/router-utils': 1.143.11 '@tanstack/virtual-file-routes': 1.145.4 - prettier: 3.8.0 + prettier: 3.7.4 recast: 0.23.11 source-map: 0.7.6 tsx: 4.21.0 @@ -3824,7 +3824,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0))': + '@tanstack/router-plugin@1.149.3(@tanstack/react-router@1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6) @@ -3832,8 +3832,8 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.28.6 '@babel/types': 7.28.6 - '@tanstack/router-core': 1.153.2 - '@tanstack/router-generator': 1.153.2 + '@tanstack/router-core': 1.149.3 + '@tanstack/router-generator': 1.149.3 '@tanstack/router-utils': 1.143.11 '@tanstack/virtual-file-routes': 1.145.4 babel-dead-code-elimination: 1.0.12 @@ -3841,8 +3841,8 @@ snapshots: unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - vite: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) + '@tanstack/react-router': 1.149.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + vite: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -3922,7 +3922,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.0.9': + '@types/node@25.0.8': dependencies: undici-types: 7.16.0 @@ -3949,11 +3949,11 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@4.2.2(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.47 - '@swc/core': 1.15.10 - vite: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) + '@swc/core': 1.15.8 + vite: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) transitivePeerDependencies: - '@swc/helpers' @@ -3998,7 +3998,7 @@ snapshots: autoprefixer@10.4.23(postcss@8.5.6): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001765 + caniuse-lite: 1.0.30001764 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 @@ -4025,7 +4025,7 @@ snapshots: balanced-match@2.0.0: {} - baseline-browser-mapping@2.9.15: {} + baseline-browser-mapping@2.9.14: {} binary-extensions@2.3.0: {} @@ -4035,21 +4035,21 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.15 - caniuse-lite: 1.0.30001765 + baseline-browser-mapping: 2.9.14 + caniuse-lite: 1.0.30001764 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) byte-size@9.0.1: {} - cacheable@2.3.2: + cacheable@2.3.1: dependencies: '@cacheable/memory': 2.0.7 '@cacheable/utils': 2.3.3 hookified: 1.15.0 keyv: 5.5.5 - qified: 0.6.0 + qified: 0.5.3 call-bind-apply-helpers@1.0.2: dependencies: @@ -4063,7 +4063,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001765: {} + caniuse-lite@1.0.30001764: {} ccount@2.0.1: {} @@ -4153,7 +4153,7 @@ snapshots: d3-ease@3.0.1: {} - d3-format@3.1.2: {} + d3-format@3.1.1: {} d3-interpolate@3.0.1: dependencies: @@ -4164,7 +4164,7 @@ snapshots: d3-scale@4.0.2: dependencies: d3-array: 3.2.4 - d3-format: 3.1.2 + d3-format: 3.1.1 d3-interpolate: 3.0.1 d3-time: 3.1.0 d3-time-format: 4.1.0 @@ -4191,7 +4191,7 @@ snapshots: decimal.js-light@2.5.1: {} - decode-named-character-reference@1.3.0: + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -4248,7 +4248,7 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - es-toolkit@1.44.0: {} + es-toolkit@1.43.0: {} esbuild@0.27.2: optionalDependencies: @@ -4285,7 +4285,7 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} - eventemitter3@5.0.4: {} + eventemitter3@5.0.1: {} extend@3.0.2: {} @@ -4311,17 +4311,17 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - file-entry-cache@11.1.2: + file-entry-cache@11.1.1: dependencies: - flat-cache: 6.1.20 + flat-cache: 6.1.19 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - flat-cache@6.1.20: + flat-cache@6.1.19: dependencies: - cacheable: 2.3.2 + cacheable: 2.3.1 flatted: 3.3.3 hookified: 1.15.0 @@ -4339,9 +4339,9 @@ snapshots: fraction.js@5.3.4: {} - framer-motion@12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.27.1 + motion-dom: 12.26.2 motion-utils: 12.24.10 tslib: 2.8.1 optionalDependencies: @@ -4567,7 +4567,7 @@ snapshots: is-plain-object@5.0.0: {} - isbot@5.1.33: {} + isbot@5.1.32: {} isexe@2.0.0: {} @@ -4622,7 +4622,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.3.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 micromark: 4.0.2 @@ -4717,7 +4717,7 @@ snapshots: micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.3.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-factory-destination: 2.0.1 micromark-factory-label: 2.0.1 @@ -4792,7 +4792,7 @@ snapshots: micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.3.0 + decode-named-character-reference: 1.2.0 micromark-util-character: 2.1.1 micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 @@ -4830,7 +4830,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 debug: 4.4.3 - decode-named-character-reference: 1.3.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 @@ -4859,15 +4859,15 @@ snapshots: dependencies: mime-db: 1.52.0 - motion-dom@12.27.1: + motion-dom@12.26.2: dependencies: motion-utils: 12.24.10 motion-utils@12.24.10: {} - motion@12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - framer-motion: 12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 optionalDependencies: react: 19.2.3 @@ -4895,7 +4895,7 @@ snapshots: '@types/unist': 2.0.11 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.3.0 + decode-named-character-reference: 1.2.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 @@ -4946,13 +4946,13 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.8.0: {} + prettier@3.7.4: {} property-information@7.1.0: {} proxy-from-env@1.1.0: {} - qified@0.6.0: + qified@0.5.3: dependencies: hookified: 1.15.0 @@ -4971,7 +4971,7 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 - react-intersection-observer@10.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-intersection-observer@10.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 optionalDependencies: @@ -5031,8 +5031,8 @@ snapshots: '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3) clsx: 2.1.1 decimal.js-light: 2.5.1 - es-toolkit: 1.44.0 - eventemitter3: 5.0.4 + es-toolkit: 1.43.0 + eventemitter3: 5.0.1 immer: 10.2.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5087,35 +5087,35 @@ snapshots: reusify@1.1.0: {} - rollup@4.55.2: + rollup@4.55.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.55.2 - '@rollup/rollup-android-arm64': 4.55.2 - '@rollup/rollup-darwin-arm64': 4.55.2 - '@rollup/rollup-darwin-x64': 4.55.2 - '@rollup/rollup-freebsd-arm64': 4.55.2 - '@rollup/rollup-freebsd-x64': 4.55.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.55.2 - '@rollup/rollup-linux-arm-musleabihf': 4.55.2 - '@rollup/rollup-linux-arm64-gnu': 4.55.2 - '@rollup/rollup-linux-arm64-musl': 4.55.2 - '@rollup/rollup-linux-loong64-gnu': 4.55.2 - '@rollup/rollup-linux-loong64-musl': 4.55.2 - '@rollup/rollup-linux-ppc64-gnu': 4.55.2 - '@rollup/rollup-linux-ppc64-musl': 4.55.2 - '@rollup/rollup-linux-riscv64-gnu': 4.55.2 - '@rollup/rollup-linux-riscv64-musl': 4.55.2 - '@rollup/rollup-linux-s390x-gnu': 4.55.2 - '@rollup/rollup-linux-x64-gnu': 4.55.2 - '@rollup/rollup-linux-x64-musl': 4.55.2 - '@rollup/rollup-openbsd-x64': 4.55.2 - '@rollup/rollup-openharmony-arm64': 4.55.2 - '@rollup/rollup-win32-arm64-msvc': 4.55.2 - '@rollup/rollup-win32-ia32-msvc': 4.55.2 - '@rollup/rollup-win32-x64-gnu': 4.55.2 - '@rollup/rollup-win32-x64-msvc': 4.55.2 + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 fsevents: 2.3.3 run-parallel@1.2.0: @@ -5319,7 +5319,7 @@ snapshots: debug: 4.4.3 fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 - file-entry-cache: 11.1.2 + file-entry-cache: 11.1.1 global-modules: 2.0.0 globby: 11.1.0 globjoin: 0.1.4 @@ -5369,98 +5369,98 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - text-camel-case@1.2.10: + text-camel-case@1.2.9: dependencies: - text-pascal-case: 1.2.10 + text-pascal-case: 1.2.9 - text-capital-case@1.2.10: + text-capital-case@1.2.9: dependencies: - text-no-case: 1.2.10 - text-upper-case-first: 1.2.10 + text-no-case: 1.2.9 + text-upper-case-first: 1.2.9 - text-case@1.2.10: + text-case@1.2.9: dependencies: - text-camel-case: 1.2.10 - text-capital-case: 1.2.10 - text-constant-case: 1.2.10 - text-dot-case: 1.2.10 - text-header-case: 1.2.10 - text-is-lower-case: 1.2.10 - text-is-upper-case: 1.2.10 - text-kebab-case: 1.2.10 - text-lower-case: 1.2.10 - text-lower-case-first: 1.2.10 - text-no-case: 1.2.10 - text-param-case: 1.2.10 - text-pascal-case: 1.2.10 - text-path-case: 1.2.10 - text-sentence-case: 1.2.10 - text-snake-case: 1.2.10 - text-swap-case: 1.2.10 - text-title-case: 1.2.10 - text-upper-case: 1.2.10 - text-upper-case-first: 1.2.10 + text-camel-case: 1.2.9 + text-capital-case: 1.2.9 + text-constant-case: 1.2.9 + text-dot-case: 1.2.9 + text-header-case: 1.2.9 + text-is-lower-case: 1.2.9 + text-is-upper-case: 1.2.9 + text-kebab-case: 1.2.9 + text-lower-case: 1.2.9 + text-lower-case-first: 1.2.9 + text-no-case: 1.2.9 + text-param-case: 1.2.9 + text-pascal-case: 1.2.9 + text-path-case: 1.2.9 + text-sentence-case: 1.2.9 + text-snake-case: 1.2.9 + text-swap-case: 1.2.9 + text-title-case: 1.2.9 + text-upper-case: 1.2.9 + text-upper-case-first: 1.2.9 - text-constant-case@1.2.10: + text-constant-case@1.2.9: dependencies: - text-no-case: 1.2.10 - text-upper-case: 1.2.10 + text-no-case: 1.2.9 + text-upper-case: 1.2.9 - text-dot-case@1.2.10: + text-dot-case@1.2.9: dependencies: - text-no-case: 1.2.10 + text-no-case: 1.2.9 - text-header-case@1.2.10: + text-header-case@1.2.9: dependencies: - text-capital-case: 1.2.10 + text-capital-case: 1.2.9 - text-is-lower-case@1.2.10: {} + text-is-lower-case@1.2.9: {} - text-is-upper-case@1.2.10: {} + text-is-upper-case@1.2.9: {} - text-kebab-case@1.2.10: + text-kebab-case@1.2.9: dependencies: - text-no-case: 1.2.10 + text-no-case: 1.2.9 - text-lower-case-first@1.2.10: {} + text-lower-case-first@1.2.9: {} - text-lower-case@1.2.10: {} + text-lower-case@1.2.9: {} - text-no-case@1.2.10: + text-no-case@1.2.9: dependencies: - text-lower-case: 1.2.10 + text-lower-case: 1.2.9 - text-param-case@1.2.10: + text-param-case@1.2.9: dependencies: - text-dot-case: 1.2.10 + text-dot-case: 1.2.9 - text-pascal-case@1.2.10: + text-pascal-case@1.2.9: dependencies: - text-no-case: 1.2.10 + text-no-case: 1.2.9 - text-path-case@1.2.10: + text-path-case@1.2.9: dependencies: - text-dot-case: 1.2.10 + text-dot-case: 1.2.9 - text-sentence-case@1.2.10: + text-sentence-case@1.2.9: dependencies: - text-no-case: 1.2.10 - text-upper-case-first: 1.2.10 + text-no-case: 1.2.9 + text-upper-case-first: 1.2.9 - text-snake-case@1.2.10: + text-snake-case@1.2.9: dependencies: - text-dot-case: 1.2.10 + text-dot-case: 1.2.9 - text-swap-case@1.2.10: {} + text-swap-case@1.2.9: {} - text-title-case@1.2.10: + text-title-case@1.2.9: dependencies: - text-no-case: 1.2.10 - text-upper-case-first: 1.2.10 + text-no-case: 1.2.9 + text-upper-case-first: 1.2.9 - text-upper-case-first@1.2.10: {} + text-upper-case-first@1.2.9: {} - text-upper-case@1.2.10: {} + text-upper-case@1.2.9: {} tiny-invariant@1.3.3: {} @@ -5582,24 +5582,24 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0)): + vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0)): dependencies: ansi-colors: 4.1.3 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0) optionalDependencies: sharp: 0.34.5 - vite@7.3.1(@types/node@25.0.9)(sass@1.97.2)(tsx@4.21.0): + vite@7.3.1(@types/node@25.0.8)(sass@1.97.2)(tsx@4.21.0): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.55.2 + rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.9 + '@types/node': 25.0.8 fsevents: 2.3.3 sass: 1.97.2 tsx: 4.21.0 From e1271e5f43600a95f8426b4c71759a662cc36c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 20 Jan 2026 09:29:03 +0100 Subject: [PATCH 50/52] update dependencies --- Cargo.lock | 78 ++++++++++++++++++++-------------------- flake.lock | 12 +++---- web/src/routeTree.gen.ts | 26 ++++++++------ 3 files changed, 60 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41004bb79d..e9797f0ff9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,7 +217,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -459,7 +459,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -1150,7 +1150,7 @@ dependencies = [ "rustls-pki-types", "serde", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "x509-parser 0.18.0", ] @@ -1181,7 +1181,7 @@ dependencies = [ "serde_cbor_2", "sqlx", "struct-patch", - "thiserror 2.0.17", + "thiserror 2.0.18", "tonic", "totp-lite", "tracing", @@ -1246,7 +1246,7 @@ dependencies = [ "strum", "strum_macros", "tera", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -1280,7 +1280,7 @@ dependencies = [ "defguard_core", "serde_json", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1292,7 +1292,7 @@ dependencies = [ "defguard_core", "defguard_event_logger", "defguard_mail", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1311,7 +1311,7 @@ dependencies = [ "serde_json", "sqlx", "tera", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1345,7 +1345,7 @@ dependencies = [ "secrecy", "semver", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tonic", @@ -1359,7 +1359,7 @@ dependencies = [ "chrono", "defguard_common", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1373,7 +1373,7 @@ dependencies = [ "os_info", "semver", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "tonic", "tower", "tracing", @@ -1799,9 +1799,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixedbitset" @@ -2691,7 +2691,7 @@ dependencies = [ "num-bigint", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "yasna", "zeroize", ] @@ -2768,7 +2768,7 @@ dependencies = [ "native-tls", "nom 7.1.3", "percent-encoding", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-native-tls", "tokio-stream", @@ -4131,7 +4131,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4152,7 +4152,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4260,9 +4260,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" dependencies = [ "pem", "ring", @@ -4554,9 +4554,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -4564,9 +4564,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -4818,7 +4818,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -5003,7 +5003,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -5131,7 +5131,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -5215,7 +5215,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -5255,7 +5255,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -5281,7 +5281,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", "uuid", @@ -5551,11 +5551,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5571,9 +5571,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -6908,7 +6908,7 @@ dependencies = [ "oid-registry 0.8.1", "ring", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -7061,9 +7061,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" [[package]] name = "zopfli" diff --git a/flake.lock b/flake.lock index 1bd30e0f57..023f36f5b1 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768127708, - "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", + "lastModified": 1768564909, + "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", + "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1768359079, - "narHash": "sha256-a016mOfKconYrYo3fZLN6c2cnmqYYd44g2bUrBZAsQc=", + "lastModified": 1768877311, + "narHash": "sha256-abSDl0cNr0B+YCsIDpO1SjXD9JMxE4s8EFnhLEFVovI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "0357d1826057686637e41147545402cbbda420ce", + "rev": "59e4ab96304585fde3890025fd59bd2717985cc1", "type": "github" }, "original": { diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index adaabe675f..9e91af3085 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -279,6 +279,7 @@ const AuthorizedDefaultLocationsLocationIdEditRoute = export interface FileRoutesByFullPath { '/404': typeof R404Route + '/': typeof AuthorizedDefaultRouteWithChildren '/auth': typeof AuthRouteWithChildren '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute @@ -313,13 +314,14 @@ export interface FileRoutesByFullPath { '/settings/smtp': typeof AuthorizedDefaultSettingsSmtpRoute '/user/$username': typeof AuthorizedDefaultUserUsernameRoute '/vpn-overview/$locationId': typeof AuthorizedDefaultVpnOverviewLocationIdRoute - '/locations': typeof AuthorizedDefaultLocationsIndexRoute - '/settings': typeof AuthorizedDefaultSettingsIndexRoute - '/vpn-overview': typeof AuthorizedDefaultVpnOverviewIndexRoute + '/locations/': typeof AuthorizedDefaultLocationsIndexRoute + '/settings/': typeof AuthorizedDefaultSettingsIndexRoute + '/vpn-overview/': typeof AuthorizedDefaultVpnOverviewIndexRoute '/locations/$locationId/edit': typeof AuthorizedDefaultLocationsLocationIdEditRoute } export interface FileRoutesByTo { '/404': typeof R404Route + '/': typeof AuthorizedDefaultRouteWithChildren '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute '/snackbar': typeof SnackbarRoute @@ -406,6 +408,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/404' + | '/' | '/auth' | '/consent' | '/playground' @@ -440,13 +443,14 @@ export interface FileRouteTypes { | '/settings/smtp' | '/user/$username' | '/vpn-overview/$locationId' - | '/locations' - | '/settings' - | '/vpn-overview' + | '/locations/' + | '/settings/' + | '/vpn-overview/' | '/locations/$locationId/edit' fileRoutesByTo: FileRoutesByTo to: | '/404' + | '/' | '/consent' | '/playground' | '/snackbar' @@ -571,7 +575,7 @@ declare module '@tanstack/react-router' { '/_authorized': { id: '/_authorized' path: '' - fullPath: '' + fullPath: '/' preLoaderRoute: typeof AuthorizedRouteImport parentRoute: typeof rootRouteImport } @@ -620,7 +624,7 @@ declare module '@tanstack/react-router' { '/_authorized/_default': { id: '/_authorized/_default' path: '' - fullPath: '' + fullPath: '/' preLoaderRoute: typeof AuthorizedDefaultRouteImport parentRoute: typeof AuthorizedRoute } @@ -718,21 +722,21 @@ declare module '@tanstack/react-router' { '/_authorized/_default/vpn-overview/': { id: '/_authorized/_default/vpn-overview/' path: '/vpn-overview' - fullPath: '/vpn-overview' + fullPath: '/vpn-overview/' preLoaderRoute: typeof AuthorizedDefaultVpnOverviewIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } '/_authorized/_default/settings/': { id: '/_authorized/_default/settings/' path: '/settings' - fullPath: '/settings' + fullPath: '/settings/' preLoaderRoute: typeof AuthorizedDefaultSettingsIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } '/_authorized/_default/locations/': { id: '/_authorized/_default/locations/' path: '/locations' - fullPath: '/locations' + fullPath: '/locations/' preLoaderRoute: typeof AuthorizedDefaultLocationsIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } From 7751270e1e1ddc5c8bd7639d02a45c7414723fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 20 Jan 2026 09:29:29 +0100 Subject: [PATCH 51/52] Revert "update dependencies" This reverts commit e1271e5f43600a95f8426b4c71759a662cc36c82. --- Cargo.lock | 78 ++++++++++++++++++++-------------------- flake.lock | 12 +++---- web/src/routeTree.gen.ts | 26 ++++++-------- 3 files changed, 56 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9797f0ff9..41004bb79d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,7 +217,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", ] @@ -459,7 +459,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.18", + "thiserror 2.0.17", ] [[package]] @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.53" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "jobserver", @@ -1150,7 +1150,7 @@ dependencies = [ "rustls-pki-types", "serde", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "x509-parser 0.18.0", ] @@ -1181,7 +1181,7 @@ dependencies = [ "serde_cbor_2", "sqlx", "struct-patch", - "thiserror 2.0.18", + "thiserror 2.0.17", "tonic", "totp-lite", "tracing", @@ -1246,7 +1246,7 @@ dependencies = [ "strum", "strum_macros", "tera", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -1280,7 +1280,7 @@ dependencies = [ "defguard_core", "serde_json", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1292,7 +1292,7 @@ dependencies = [ "defguard_core", "defguard_event_logger", "defguard_mail", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1311,7 +1311,7 @@ dependencies = [ "serde_json", "sqlx", "tera", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1345,7 +1345,7 @@ dependencies = [ "secrecy", "semver", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tokio-stream", "tonic", @@ -1359,7 +1359,7 @@ dependencies = [ "chrono", "defguard_common", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1373,7 +1373,7 @@ dependencies = [ "os_info", "semver", "serde", - "thiserror 2.0.18", + "thiserror 2.0.17", "tonic", "tower", "tracing", @@ -1799,9 +1799,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fixedbitset" @@ -2691,7 +2691,7 @@ dependencies = [ "num-bigint", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.17", "yasna", "zeroize", ] @@ -2768,7 +2768,7 @@ dependencies = [ "native-tls", "nom 7.1.3", "percent-encoding", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tokio-native-tls", "tokio-stream", @@ -4131,7 +4131,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -4152,7 +4152,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -4260,9 +4260,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.7" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" dependencies = [ "pem", "ring", @@ -4554,9 +4554,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc" dependencies = [ "web-time", "zeroize", @@ -4564,9 +4564,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -4818,7 +4818,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.18", + "thiserror 2.0.17", ] [[package]] @@ -5003,7 +5003,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", ] @@ -5131,7 +5131,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -5215,7 +5215,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -5255,7 +5255,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -5281,7 +5281,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.18", + "thiserror 2.0.17", "tracing", "url", "uuid", @@ -5551,11 +5551,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.17", ] [[package]] @@ -5571,9 +5571,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -6908,7 +6908,7 @@ dependencies = [ "oid-registry 0.8.1", "ring", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", ] @@ -7061,9 +7061,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" [[package]] name = "zopfli" diff --git a/flake.lock b/flake.lock index 023f36f5b1..1bd30e0f57 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768564909, - "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "lastModified": 1768127708, + "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1768877311, - "narHash": "sha256-abSDl0cNr0B+YCsIDpO1SjXD9JMxE4s8EFnhLEFVovI=", + "lastModified": 1768359079, + "narHash": "sha256-a016mOfKconYrYo3fZLN6c2cnmqYYd44g2bUrBZAsQc=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "59e4ab96304585fde3890025fd59bd2717985cc1", + "rev": "0357d1826057686637e41147545402cbbda420ce", "type": "github" }, "original": { diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 9e91af3085..adaabe675f 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -279,7 +279,6 @@ const AuthorizedDefaultLocationsLocationIdEditRoute = export interface FileRoutesByFullPath { '/404': typeof R404Route - '/': typeof AuthorizedDefaultRouteWithChildren '/auth': typeof AuthRouteWithChildren '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute @@ -314,14 +313,13 @@ export interface FileRoutesByFullPath { '/settings/smtp': typeof AuthorizedDefaultSettingsSmtpRoute '/user/$username': typeof AuthorizedDefaultUserUsernameRoute '/vpn-overview/$locationId': typeof AuthorizedDefaultVpnOverviewLocationIdRoute - '/locations/': typeof AuthorizedDefaultLocationsIndexRoute - '/settings/': typeof AuthorizedDefaultSettingsIndexRoute - '/vpn-overview/': typeof AuthorizedDefaultVpnOverviewIndexRoute + '/locations': typeof AuthorizedDefaultLocationsIndexRoute + '/settings': typeof AuthorizedDefaultSettingsIndexRoute + '/vpn-overview': typeof AuthorizedDefaultVpnOverviewIndexRoute '/locations/$locationId/edit': typeof AuthorizedDefaultLocationsLocationIdEditRoute } export interface FileRoutesByTo { '/404': typeof R404Route - '/': typeof AuthorizedDefaultRouteWithChildren '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute '/snackbar': typeof SnackbarRoute @@ -408,7 +406,6 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/404' - | '/' | '/auth' | '/consent' | '/playground' @@ -443,14 +440,13 @@ export interface FileRouteTypes { | '/settings/smtp' | '/user/$username' | '/vpn-overview/$locationId' - | '/locations/' - | '/settings/' - | '/vpn-overview/' + | '/locations' + | '/settings' + | '/vpn-overview' | '/locations/$locationId/edit' fileRoutesByTo: FileRoutesByTo to: | '/404' - | '/' | '/consent' | '/playground' | '/snackbar' @@ -575,7 +571,7 @@ declare module '@tanstack/react-router' { '/_authorized': { id: '/_authorized' path: '' - fullPath: '/' + fullPath: '' preLoaderRoute: typeof AuthorizedRouteImport parentRoute: typeof rootRouteImport } @@ -624,7 +620,7 @@ declare module '@tanstack/react-router' { '/_authorized/_default': { id: '/_authorized/_default' path: '' - fullPath: '/' + fullPath: '' preLoaderRoute: typeof AuthorizedDefaultRouteImport parentRoute: typeof AuthorizedRoute } @@ -722,21 +718,21 @@ declare module '@tanstack/react-router' { '/_authorized/_default/vpn-overview/': { id: '/_authorized/_default/vpn-overview/' path: '/vpn-overview' - fullPath: '/vpn-overview/' + fullPath: '/vpn-overview' preLoaderRoute: typeof AuthorizedDefaultVpnOverviewIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } '/_authorized/_default/settings/': { id: '/_authorized/_default/settings/' path: '/settings' - fullPath: '/settings/' + fullPath: '/settings' preLoaderRoute: typeof AuthorizedDefaultSettingsIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } '/_authorized/_default/locations/': { id: '/_authorized/_default/locations/' path: '/locations' - fullPath: '/locations/' + fullPath: '/locations' preLoaderRoute: typeof AuthorizedDefaultLocationsIndexRouteImport parentRoute: typeof AuthorizedDefaultRoute } From 81c8673f351a89b29f710ca0e39a00dce18d6fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 20 Jan 2026 09:29:59 +0100 Subject: [PATCH 52/52] update backend dependencies --- Cargo.lock | 78 +++++++++++++++++++++++++++--------------------------- flake.lock | 12 ++++----- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41004bb79d..e9797f0ff9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,7 +217,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -459,7 +459,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -1150,7 +1150,7 @@ dependencies = [ "rustls-pki-types", "serde", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "x509-parser 0.18.0", ] @@ -1181,7 +1181,7 @@ dependencies = [ "serde_cbor_2", "sqlx", "struct-patch", - "thiserror 2.0.17", + "thiserror 2.0.18", "tonic", "totp-lite", "tracing", @@ -1246,7 +1246,7 @@ dependencies = [ "strum", "strum_macros", "tera", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -1280,7 +1280,7 @@ dependencies = [ "defguard_core", "serde_json", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1292,7 +1292,7 @@ dependencies = [ "defguard_core", "defguard_event_logger", "defguard_mail", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1311,7 +1311,7 @@ dependencies = [ "serde_json", "sqlx", "tera", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1345,7 +1345,7 @@ dependencies = [ "secrecy", "semver", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tonic", @@ -1359,7 +1359,7 @@ dependencies = [ "chrono", "defguard_common", "sqlx", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1373,7 +1373,7 @@ dependencies = [ "os_info", "semver", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "tonic", "tower", "tracing", @@ -1799,9 +1799,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixedbitset" @@ -2691,7 +2691,7 @@ dependencies = [ "num-bigint", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "yasna", "zeroize", ] @@ -2768,7 +2768,7 @@ dependencies = [ "native-tls", "nom 7.1.3", "percent-encoding", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-native-tls", "tokio-stream", @@ -4131,7 +4131,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4152,7 +4152,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4260,9 +4260,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" dependencies = [ "pem", "ring", @@ -4554,9 +4554,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -4564,9 +4564,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -4818,7 +4818,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -5003,7 +5003,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -5131,7 +5131,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -5215,7 +5215,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -5255,7 +5255,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -5281,7 +5281,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", "uuid", @@ -5551,11 +5551,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5571,9 +5571,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -6908,7 +6908,7 @@ dependencies = [ "oid-registry 0.8.1", "ring", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -7061,9 +7061,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" [[package]] name = "zopfli" diff --git a/flake.lock b/flake.lock index 1bd30e0f57..023f36f5b1 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768127708, - "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", + "lastModified": 1768564909, + "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", + "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1768359079, - "narHash": "sha256-a016mOfKconYrYo3fZLN6c2cnmqYYd44g2bUrBZAsQc=", + "lastModified": 1768877311, + "narHash": "sha256-abSDl0cNr0B+YCsIDpO1SjXD9JMxE4s8EFnhLEFVovI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "0357d1826057686637e41147545402cbbda420ce", + "rev": "59e4ab96304585fde3890025fd59bd2717985cc1", "type": "github" }, "original": {