diff --git a/Cargo.lock b/Cargo.lock index b919e6d459..9f6109695d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,9 +442,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" dependencies = [ "serde", ] @@ -1047,6 +1047,7 @@ dependencies = [ "dotenvy", "ed25519-dalek", "humantime", + "hyper-util", "ipnetwork", "jsonwebkey", "jsonwebtoken", @@ -1769,7 +1770,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "libc", "libgit2-sys", "log", @@ -1785,8 +1786,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -1795,7 +1796,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "ignore", "walkdir", ] @@ -1823,7 +1824,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.10.0", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", @@ -2264,7 +2265,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.9", + "regex-automata 0.4.10", "same-file", "walkdir", "winapi-util", @@ -2283,9 +2284,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown 0.15.5", @@ -2303,11 +2304,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "cfg-if", "libc", ] @@ -2375,9 +2376,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -2549,7 +2550,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "libc", "redox_syscall", ] @@ -3011,7 +3012,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "cfg-if", "foreign-types", "libc", @@ -3282,7 +3283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.11.0", ] [[package]] @@ -3455,7 +3456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", - "indexmap 2.10.0", + "indexmap 2.11.0", "quick-xml", "serde", "time", @@ -3619,7 +3620,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "getopts", "memchr", "pulldown-cmark-escape", @@ -3643,9 +3644,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.2" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] @@ -3791,7 +3792,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] @@ -3816,14 +3817,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -3837,13 +3838,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] @@ -3854,9 +3855,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" @@ -4044,7 +4045,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys", @@ -4189,7 +4190,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4202,7 +4203,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4281,7 +4282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" dependencies = [ "form_urlencoded", - "indexmap 2.10.0", + "indexmap 2.11.0", "itoa", "ryu", "serde", @@ -4351,7 +4352,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.11.0", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -4379,7 +4380,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "itoa", "ryu", "serde", @@ -4598,7 +4599,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.10.0", + "indexmap 2.11.0", "ipnetwork", "log", "memchr", @@ -4663,7 +4664,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.2", + "bitflags 2.9.3", "byteorder", "bytes", "chrono", @@ -4707,7 +4708,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.2", + "bitflags 2.9.3", "byteorder", "chrono", "crc", @@ -4926,7 +4927,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5179,7 +5180,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "toml_datetime", "winnow", ] @@ -5288,7 +5289,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.10.0", + "indexmap 2.11.0", "pin-project-lite", "slab", "sync_wrapper", @@ -5305,7 +5306,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "bytes", "futures-core", "futures-util", @@ -5578,9 +5579,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -5606,7 +5607,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "serde", "serde_json", "utoipa-gen", @@ -6308,9 +6309,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -6321,7 +6322,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] @@ -6512,7 +6513,7 @@ dependencies = [ "flate2", "getrandom 0.3.3", "hmac", - "indexmap 2.10.0", + "indexmap 2.11.0", "lzma-rs", "memchr", "pbkdf2", @@ -6534,7 +6535,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.10.0", + "indexmap 2.11.0", "memchr", "zopfli", ] diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 9ec084cd17..d8b47a1a89 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -20,7 +20,10 @@ use defguard_core::{ limits::update_counts, }, events::{ApiEvent, BidiStreamEvent, GrpcEvent, InternalEvent}, - grpc::{GatewayMap, WorkerState, run_grpc_bidi_stream, run_grpc_server}, + grpc::{ + GatewayMap, WorkerState, gateway::client_state::ClientMap, run_grpc_bidi_stream, + run_grpc_server, + }, init_dev_env, init_vpn_location, mail::{Mail, run_mail_handler}, run_web_server, @@ -102,6 +105,7 @@ async fn main() -> Result<(), anyhow::Error> { let worker_state = Arc::new(Mutex::new(WorkerState::new(webhook_tx.clone()))); let gateway_state = Arc::new(Mutex::new(GatewayMap::new())); + let client_state = Arc::new(Mutex::new(ClientMap::new())); // initialize admin user User::init_admin_user(&pool, config.default_admin_password.expose_secret()).await?; @@ -144,7 +148,7 @@ async fn main() -> Result<(), anyhow::Error> { // run services tokio::select! { res = run_grpc_bidi_stream(pool.clone(), wireguard_tx.clone(), mail_tx.clone(), bidi_event_tx), if config.proxy_url.is_some() => error!("Proxy gRPC stream returned early: {res:?}"), - res = run_grpc_server(Arc::clone(&worker_state), pool.clone(), Arc::clone(&gateway_state), wireguard_tx.clone(), mail_tx.clone(), grpc_cert, grpc_key, failed_logins.clone(), grpc_event_tx) => error!("gRPC server returned early: {res:?}"), + res = run_grpc_server(Arc::clone(&worker_state), pool.clone(), Arc::clone(&gateway_state), client_state, wireguard_tx.clone(), mail_tx.clone(), grpc_cert, grpc_key, failed_logins.clone(), grpc_event_tx) => error!("gRPC server returned early: {res:?}"), res = run_web_server(worker_state, gateway_state, webhook_tx, webhook_rx, wireguard_tx.clone(), mail_tx.clone(), pool.clone(), failed_logins, api_event_tx) => error!("Web server returned early: {res:?}"), res = run_mail_handler(mail_rx) => error!("Mail handler returned early: {res:?}"), res = run_periodic_peer_disconnect(pool.clone(), wireguard_tx.clone(), internal_event_tx.clone()) => error!("Periodic peer disconnect task returned early: {res:?}"), diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index 62d908fb6d..15cfc434d8 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -95,6 +95,7 @@ version = "=2.4.2" [dev-dependencies] bytes = "1.6" claims = "0.8" +hyper-util = "0.1" matches = "0.1" regex = "1.10" reqwest = { version = "0.12", features = [ @@ -105,6 +106,7 @@ reqwest = { version = "0.12", features = [ "stream", ], default-features = false } serde_qs = "0.13" +tower = "0.5" webauthn-authenticator-rs = { version = "0.5", features = ["softpasskey"] } [build-dependencies] diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs index 67d996dd10..1996fcdd69 100644 --- a/crates/defguard_core/src/db/models/wireguard.rs +++ b/crates/defguard_core/src/db/models/wireguard.rs @@ -30,6 +30,7 @@ use super::{ }; use crate::{ AsCsv, + auth::{Claims, ClaimsType}, db::{Id, NoId}, enterprise::firewall::FirewallError, grpc::{ @@ -213,6 +214,8 @@ pub enum WireguardNetworkError { DeviceError(#[from] DeviceError), #[error("Firewall config error: {0}")] FirewallError(#[from] FirewallError), + #[error(transparent)] + TokenError(#[from] jsonwebtoken::errors::Error), } #[derive(Debug, Error)] @@ -1303,6 +1306,21 @@ impl WireguardNetwork { Ok(locations) } + + /// Generates auth token for a VPN gateway + pub fn generate_gateway_token(&self) -> Result { + let location_id = self.id; + + let token = Claims::new( + ClaimsType::Gateway, + format!("DEFGUARD-NETWORK-{location_id}"), + location_id.to_string(), + u32::MAX.into(), + ) + .to_jwt()?; + + Ok(token) + } } // [`IpNetwork`] does not implement [`Default`] diff --git a/crates/defguard_core/src/error.rs b/crates/defguard_core/src/error.rs index 162c04ab1a..c7888906a0 100644 --- a/crates/defguard_core/src/error.rs +++ b/crates/defguard_core/src/error.rs @@ -140,9 +140,8 @@ impl From for WebError { | WireguardNetworkError::Unexpected(_) | WireguardNetworkError::DeviceError(_) | WireguardNetworkError::DeviceNotAllowed(_) - | WireguardNetworkError::FirewallError(_) => { - Self::Http(StatusCode::INTERNAL_SERVER_ERROR) - } + | WireguardNetworkError::FirewallError(_) + | WireguardNetworkError::TokenError(_) => Self::Http(StatusCode::INTERNAL_SERVER_ERROR), } } } diff --git a/crates/defguard_core/src/grpc/gateway/client_state.rs b/crates/defguard_core/src/grpc/gateway/client_state.rs index a57ab38eab..8eb1e600f4 100644 --- a/crates/defguard_core/src/grpc/gateway/client_state.rs +++ b/crates/defguard_core/src/grpc/gateway/client_state.rs @@ -84,7 +84,7 @@ impl ClientState { /// Helper struct used to handle connected VPN clients state /// Clients are grouped by location ID type ClientPubKey = String; -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Default, Serialize, Clone)] pub struct ClientMap(HashMap>); impl ClientMap { @@ -195,4 +195,8 @@ impl ClientMap { Ok(disconnected_clients) } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 5acb8b759c..4f774740b3 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -1,4 +1,4 @@ -mod client_state; +pub mod client_state; use std::{ net::{IpAddr, SocketAddr}, pin::Pin, @@ -141,15 +141,16 @@ impl GatewayServer { #[must_use] pub fn new( pool: PgPool, - state: Arc>, + gateway_state: Arc>, + client_state: Arc>, wireguard_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, ) -> Self { Self { pool, - gateway_state: state, - client_state: Arc::new(Mutex::new(ClientMap::new())), + gateway_state, + client_state, wireguard_tx, mail_tx, grpc_event_tx, @@ -1024,7 +1025,7 @@ impl gateway_service_server::GatewayService for GatewayServer { error!("Failed to connect gateway on network {network_id}: {err}"); Status::new( Code::Internal, - "Failed to connect gateway on network {gateway_network_id}", + format!("Failed to connect gateway on network {network_id}"), ) })?; diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index 427d634f4a..995772db22 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -31,7 +31,9 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::sync::CancellationToken; use tonic::{ Code, Status, Streaming, - transport::{Certificate, ClientTlsConfig, Endpoint, Identity, Server, ServerTlsConfig}, + transport::{ + Certificate, ClientTlsConfig, Endpoint, Identity, Server, ServerTlsConfig, server::Router, + }, }; use tower::ServiceBuilder; use utoipa::ToSchema; @@ -67,6 +69,7 @@ use crate::{ ldap::utils::ldap_update_user_state, }, events::{BidiStreamEvent, GrpcEvent}, + grpc::gateway::client_state::ClientMap, handlers::mail::{send_gateway_disconnected_email, send_gateway_reconnected_email}, mail::Mail, server_config, @@ -78,7 +81,7 @@ mod auth; pub(crate) mod client_mfa; pub mod enrollment; #[cfg(feature = "wireguard")] -pub(crate) mod gateway; +pub mod gateway; #[cfg(any(feature = "wireguard", feature = "worker"))] mod interceptor; pub mod password_reset; @@ -142,6 +145,10 @@ impl GatewayMap { Self(HashMap::new()) } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + // add a new gateway to map // this method is meant to be called when a gateway requests a config // as a sort of "registration" @@ -934,6 +941,7 @@ pub async fn run_grpc_server( worker_state: Arc>, pool: PgPool, gateway_state: Arc>, + client_state: Arc>, wireguard_tx: Sender, mail_tx: UnboundedSender, grpc_cert: Option, @@ -942,15 +950,68 @@ pub async fn run_grpc_server( grpc_event_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { // Build gRPC services + let server = if let (Some(cert), Some(key)) = (grpc_cert, grpc_key) { + let identity = Identity::from_pem(cert, key); + Server::builder().tls_config(ServerTlsConfig::new().identity(identity))? + } else { + Server::builder() + }; + + let router = build_grpc_service_router( + server, + pool, + worker_state, + gateway_state, + client_state, + wireguard_tx, + mail_tx, + failed_logins, + grpc_event_tx, + ) + .await?; + + // Run gRPC server + let addr = SocketAddr::new( + server_config() + .grpc_bind_address + .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)), + server_config().grpc_port, + ); + debug!("Starting gRPC services"); + router.serve(addr).await?; + info!("gRPC server started on {addr}"); + Ok(()) +} + +pub async fn build_grpc_service_router( + server: Server, + pool: PgPool, + worker_state: Arc>, + gateway_state: Arc>, + client_state: Arc>, + wireguard_tx: Sender, + mail_tx: UnboundedSender, + failed_logins: Arc>, + grpc_event_tx: UnboundedSender, +) -> Result { let auth_service = AuthServiceServer::new(AuthServer::new(pool.clone(), failed_logins)); + #[cfg(feature = "worker")] let worker_service = WorkerServiceServer::with_interceptor( WorkerServer::new(pool.clone(), worker_state), JwtInterceptor::new(ClaimsType::YubiBridge), ); + #[cfg(feature = "wireguard")] let gateway_service = GatewayServiceServer::with_interceptor( - GatewayServer::new(pool, gateway_state, wireguard_tx, mail_tx, grpc_event_tx), + GatewayServer::new( + pool, + gateway_state, + client_state, + wireguard_tx, + mail_tx, + grpc_event_tx, + ), JwtInterceptor::new(ClaimsType::Gateway), ); @@ -959,37 +1020,23 @@ pub async fn run_grpc_server( .set_serving::>() .await; - // Run gRPC server - let addr = SocketAddr::new( - server_config() - .grpc_bind_address - .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)), - server_config().grpc_port, - ); - debug!("Starting gRPC services"); - let builder = if let (Some(cert), Some(key)) = (grpc_cert, grpc_key) { - let identity = Identity::from_pem(cert, key); - Server::builder().tls_config(ServerTlsConfig::new().identity(identity))? - } else { - Server::builder() - }; - - let router = builder + let router = server .http2_keepalive_interval(Some(TEN_SECS)) .tcp_keepalive(Some(TEN_SECS)) .add_service(health_service) .add_service(auth_service); + #[cfg(feature = "wireguard")] let router = router.add_service( ServiceBuilder::new() .layer(DefguardVersionLayer::new(Version::parse(VERSION)?)) .service(gateway_service), ); + #[cfg(feature = "worker")] let router = router.add_service(worker_service); - router.serve(addr).await?; - info!("gRPC server started on {addr}"); - Ok(()) + + Ok(router) } #[cfg(feature = "worker")] diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 54e4d27bd8..929bd88f05 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -21,7 +21,7 @@ use super::{ApiResponse, ApiResult, WebError, device_for_admin_or_self, user_for use crate::{ AsCsv, appstate::AppState, - auth::{AdminRole, Claims, ClaimsType, SessionInfo}, + auth::{AdminRole, SessionInfo}, db::{ AddDevice, Device, GatewayEvent, Id, WireguardNetwork, models::{ @@ -1244,14 +1244,7 @@ pub(crate) async fn create_network_token( ) -> ApiResult { debug!("Generating a new token for network ID {network_id}"); let network = find_network(network_id, &appstate.pool).await?; - let token = Claims::new( - ClaimsType::Gateway, - format!("DEFGUARD-NETWORK-{network_id}"), - network_id.to_string(), - u32::MAX.into(), - ) - .to_jwt() - .map_err(|_| { + let token = network.generate_gateway_token().map_err(|_| { error!("Failed to create token for gateway {}", network.name); WebError::Authorization(format!( "Failed to create token for gateway {}", diff --git a/crates/defguard_core/tests/integration/acl.rs b/crates/defguard_core/tests/integration/api/acl.rs similarity index 99% rename from crates/defguard_core/tests/integration/acl.rs rename to crates/defguard_core/tests/integration/api/acl.rs index 103c69c845..636695c91c 100644 --- a/crates/defguard_core/tests/integration/acl.rs +++ b/crates/defguard_core/tests/integration/api/acl.rs @@ -21,9 +21,11 @@ use sqlx::{ }; use tokio::net::TcpListener; -use crate::common::{ - authenticate_admin, client::TestClient, exceed_enterprise_limits, init_config, - initialize_users, make_base_client, make_test_client, omit_id, setup_pool, +use crate::common::{init_config, initialize_users}; + +use super::common::{ + authenticate_admin, client::TestClient, exceed_enterprise_limits, make_base_client, + make_test_client, omit_id, setup_pool, }; async fn make_client_v2(pool: PgPool, config: DefGuardConfig) -> TestClient { diff --git a/crates/defguard_core/tests/integration/api_tokens.rs b/crates/defguard_core/tests/integration/api/api_tokens.rs similarity index 99% rename from crates/defguard_core/tests/integration/api_tokens.rs rename to crates/defguard_core/tests/integration/api/api_tokens.rs index f7805efa50..0c5ab07893 100644 --- a/crates/defguard_core/tests/integration/api_tokens.rs +++ b/crates/defguard_core/tests/integration/api/api_tokens.rs @@ -11,7 +11,7 @@ use reqwest::{StatusCode, header::HeaderName}; use serde::Deserialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{make_client, make_client_with_state, setup_pool}; +use super::common::{make_client, make_client_with_state, setup_pool}; #[sqlx::test] async fn test_normal_user_cannot_access_token_endpoints( diff --git a/crates/defguard_core/tests/integration/auth.rs b/crates/defguard_core/tests/integration/api/auth.rs similarity index 99% rename from crates/defguard_core/tests/integration/auth.rs rename to crates/defguard_core/tests/integration/api/auth.rs index 4ac9279557..c6f29abd6c 100644 --- a/crates/defguard_core/tests/integration/auth.rs +++ b/crates/defguard_core/tests/integration/api/auth.rs @@ -20,7 +20,7 @@ use totp_lite::{Sha1, totp_custom}; use webauthn_authenticator_rs::{WebauthnAuthenticator, prelude::Url, softpasskey::SoftPasskey}; use webauthn_rs::prelude::{CreationChallengeResponse, RequestChallengeResponse}; -use crate::common::{ +use super::common::{ X_FORWARDED_FOR, fetch_user_details, make_client, make_client_with_db, make_client_with_state, make_test_client, setup_pool, }; diff --git a/crates/defguard_core/tests/integration/common/client.rs b/crates/defguard_core/tests/integration/api/common/client.rs similarity index 94% rename from crates/defguard_core/tests/integration/common/client.rs rename to crates/defguard_core/tests/integration/api/common/client.rs index bcf680cca3..61a45135dc 100644 --- a/crates/defguard_core/tests/integration/common/client.rs +++ b/crates/defguard_core/tests/integration/api/common/client.rs @@ -9,7 +9,7 @@ use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT}, redirect::Policy, }; -use tokio::{net::TcpListener, sync::mpsc::UnboundedReceiver}; +use tokio::{net::TcpListener, sync::mpsc::UnboundedReceiver, task::JoinHandle}; pub struct TestClient { client: Client, @@ -18,6 +18,7 @@ pub struct TestClient { // Has to live during whole test #[allow(dead_code)] api_event_rx: UnboundedReceiver, + api_task_handle: JoinHandle<()>, } impl TestClient { @@ -29,7 +30,7 @@ impl TestClient { ) -> Self { let port = listener.local_addr().unwrap().port(); - tokio::spawn(async move { + let api_task_handle = tokio::spawn(async move { let server = serve( listener, app.into_make_service_with_connect_info::(), @@ -54,6 +55,7 @@ impl TestClient { jar, port, api_event_rx, + api_task_handle, } } @@ -122,6 +124,13 @@ impl TestClient { } } +impl Drop for TestClient { + fn drop(&mut self) { + // explicitly stop spawned API server task + self.api_task_handle.abort(); + } +} + pub struct RequestBuilder { builder: reqwest::RequestBuilder, } diff --git a/crates/defguard_core/tests/integration/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs similarity index 87% rename from crates/defguard_core/tests/integration/common/mod.rs rename to crates/defguard_core/tests/integration/api/common/mod.rs index 32c71664c1..8da63d186d 100644 --- a/crates/defguard_core/tests/integration/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -1,13 +1,9 @@ pub(crate) mod client; -use std::{ - str::FromStr, - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, Mutex}; pub use defguard_core::db::setup_pool; use defguard_core::{ - SERVER_CONFIG, auth::failed_login::FailedLoginMap, build_webapp, config::DefGuardConfig, @@ -21,8 +17,7 @@ use defguard_core::{ handlers::Auth, mail::Mail, }; -use reqwest::{StatusCode, Url, header::HeaderName}; -use secrecy::ExposeSecret; +use reqwest::{StatusCode, header::HeaderName}; use serde::de::DeserializeOwned; use serde_json::{Value, json}; use sqlx::PgPool; @@ -34,6 +29,8 @@ use tokio::{ }, }; +use crate::common::{init_config, initialize_users}; + use self::client::TestClient; #[allow(clippy::declare_interior_mutable_const)] @@ -43,34 +40,6 @@ pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for #[allow(clippy::declare_interior_mutable_const)] pub const X_FORWARDED_URI: HeaderName = HeaderName::from_static("x-forwarded-uri"); -/// Allows overriding the default DefGuard URL for tests, as during the tests, the server has a random port, making the URL unpredictable beforehand. -// TODO: Allow customizing the whole config, not just the URL -pub(crate) fn init_config(custom_defguard_url: Option<&str>) -> DefGuardConfig { - let url = custom_defguard_url.unwrap_or("http://localhost:8000"); - let mut config = DefGuardConfig::new_test_config(); - config.url = Url::from_str(url).unwrap(); - let _ = SERVER_CONFIG.set(config.clone()); - config -} - -pub(crate) async fn initialize_users(pool: &PgPool, config: &DefGuardConfig) { - User::init_admin_user(pool, config.default_admin_password.expose_secret()) - .await - .unwrap(); - - User::new( - "hpotter", - Some("pass123"), - "Potter", - "Harry", - "h.potter@hogwart.edu.uk", - None, - ) - .save(pool) - .await - .unwrap(); -} - pub(crate) struct ClientState { pub pool: PgPool, pub worker_state: Arc>, diff --git a/crates/defguard_core/tests/integration/enrollment.rs b/crates/defguard_core/tests/integration/api/enrollment.rs similarity index 98% rename from crates/defguard_core/tests/integration/enrollment.rs rename to crates/defguard_core/tests/integration/api/enrollment.rs index 71eeb1eceb..7068650a3d 100644 --- a/crates/defguard_core/tests/integration/enrollment.rs +++ b/crates/defguard_core/tests/integration/api/enrollment.rs @@ -7,7 +7,7 @@ use serde::Deserialize; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{fetch_user_details, make_client_with_db, setup_pool}; +use super::common::{fetch_user_details, make_client_with_db, setup_pool}; #[sqlx::test] async fn test_initialize_enrollment(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/enterprise_settings.rs b/crates/defguard_core/tests/integration/api/enterprise_settings.rs similarity index 99% rename from crates/defguard_core/tests/integration/enterprise_settings.rs rename to crates/defguard_core/tests/integration/api/enterprise_settings.rs index 821899c6b0..7634964196 100644 --- a/crates/defguard_core/tests/integration/enterprise_settings.rs +++ b/crates/defguard_core/tests/integration/api/enterprise_settings.rs @@ -9,7 +9,7 @@ use reqwest::StatusCode; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{exceed_enterprise_limits, make_network, make_test_client, setup_pool}; +use super::common::{exceed_enterprise_limits, make_network, make_test_client, setup_pool}; #[sqlx::test] async fn test_only_enterprise_can_modify(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/forward_auth.rs b/crates/defguard_core/tests/integration/api/forward_auth.rs similarity index 96% rename from crates/defguard_core/tests/integration/forward_auth.rs rename to crates/defguard_core/tests/integration/api/forward_auth.rs index fce53bc90d..83b97c1ba9 100644 --- a/crates/defguard_core/tests/integration/forward_auth.rs +++ b/crates/defguard_core/tests/integration/api/forward_auth.rs @@ -2,7 +2,7 @@ use defguard_core::{SERVER_CONFIG, handlers::Auth}; use reqwest::StatusCode; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{X_FORWARDED_HOST, X_FORWARDED_URI, make_client, setup_pool}; +use super::common::{X_FORWARDED_HOST, X_FORWARDED_URI, make_client, setup_pool}; #[sqlx::test] async fn test_forward_auth(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/group.rs b/crates/defguard_core/tests/integration/api/group.rs similarity index 99% rename from crates/defguard_core/tests/integration/group.rs rename to crates/defguard_core/tests/integration/api/group.rs index 42ea560e02..f96d78904b 100644 --- a/crates/defguard_core/tests/integration/group.rs +++ b/crates/defguard_core/tests/integration/api/group.rs @@ -3,7 +3,7 @@ use reqwest::StatusCode; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{make_test_client, setup_pool}; +use super::common::{make_test_client, setup_pool}; #[sqlx::test] async fn test_create_group(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/api/mod.rs b/crates/defguard_core/tests/integration/api/mod.rs new file mode 100644 index 0000000000..4891501678 --- /dev/null +++ b/crates/defguard_core/tests/integration/api/mod.rs @@ -0,0 +1,21 @@ +mod acl; +mod api_tokens; +mod auth; +mod common; +mod enrollment; +mod enterprise_settings; +mod forward_auth; +mod group; +mod oauth; +mod openid; +mod openid_login; +mod settings; +mod snat; +mod user; +mod webhook; +mod wireguard; +mod wireguard_network_allowed_groups; +mod wireguard_network_devices; +mod wireguard_network_import; +mod wireguard_network_stats; +mod worker; diff --git a/crates/defguard_core/tests/integration/oauth.rs b/crates/defguard_core/tests/integration/api/oauth.rs similarity index 99% rename from crates/defguard_core/tests/integration/oauth.rs rename to crates/defguard_core/tests/integration/api/oauth.rs index ec5e4363c2..06d86dbe3b 100644 --- a/crates/defguard_core/tests/integration/oauth.rs +++ b/crates/defguard_core/tests/integration/api/oauth.rs @@ -14,7 +14,7 @@ use reqwest::{StatusCode, Url, header::CONTENT_TYPE}; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{make_client_with_db, setup_pool}; +use super::common::{make_client_with_db, setup_pool}; #[sqlx::test] async fn test_authorize(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/openid.rs b/crates/defguard_core/tests/integration/api/openid.rs similarity index 99% rename from crates/defguard_core/tests/integration/openid.rs rename to crates/defguard_core/tests/integration/api/openid.rs index 30f34ee78c..3a2b141d5a 100644 --- a/crates/defguard_core/tests/integration/openid.rs +++ b/crates/defguard_core/tests/integration/api/openid.rs @@ -26,7 +26,7 @@ use rsa::RsaPrivateKey; use serde::Deserialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{ +use super::common::{ client::TestClient, make_client, make_client_with_state, make_test_client, setup_pool, }; diff --git a/crates/defguard_core/tests/integration/openid_login.rs b/crates/defguard_core/tests/integration/api/openid_login.rs similarity index 99% rename from crates/defguard_core/tests/integration/openid_login.rs rename to crates/defguard_core/tests/integration/api/openid_login.rs index 2a11304f2e..6a40ec60d5 100644 --- a/crates/defguard_core/tests/integration/openid_login.rs +++ b/crates/defguard_core/tests/integration/api/openid_login.rs @@ -12,7 +12,7 @@ use reqwest::{StatusCode, Url}; use serde::Deserialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{exceed_enterprise_limits, make_client, setup_pool}; +use super::common::{exceed_enterprise_limits, make_client, setup_pool}; // Temporarily disabled because of the issue with test_openid_login // async fn make_client_with_real_url() -> TestClient { diff --git a/crates/defguard_core/tests/integration/settings.rs b/crates/defguard_core/tests/integration/api/settings.rs similarity index 96% rename from crates/defguard_core/tests/integration/settings.rs rename to crates/defguard_core/tests/integration/api/settings.rs index 1952708765..a832b60eb8 100644 --- a/crates/defguard_core/tests/integration/settings.rs +++ b/crates/defguard_core/tests/integration/api/settings.rs @@ -5,7 +5,7 @@ use defguard_core::{ use reqwest::StatusCode; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{make_client_with_state, setup_pool}; +use super::common::{make_client_with_state, setup_pool}; #[sqlx::test] async fn test_settings(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/snat.rs b/crates/defguard_core/tests/integration/api/snat.rs similarity index 99% rename from crates/defguard_core/tests/integration/snat.rs rename to crates/defguard_core/tests/integration/api/snat.rs index 9fb4f2abb7..a396041d4c 100644 --- a/crates/defguard_core/tests/integration/snat.rs +++ b/crates/defguard_core/tests/integration/api/snat.rs @@ -12,7 +12,7 @@ use defguard_core::{ use reqwest::StatusCode; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{ +use super::common::{ authenticate_admin, exceed_enterprise_limits, make_network, make_test_client, setup_pool, }; diff --git a/crates/defguard_core/tests/integration/user.rs b/crates/defguard_core/tests/integration/api/user.rs similarity index 99% rename from crates/defguard_core/tests/integration/user.rs rename to crates/defguard_core/tests/integration/api/user.rs index f1f8f85c38..a7433cacde 100644 --- a/crates/defguard_core/tests/integration/user.rs +++ b/crates/defguard_core/tests/integration/api/user.rs @@ -9,7 +9,7 @@ use reqwest::{StatusCode, header::USER_AGENT}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use tokio_stream::{self as stream, StreamExt}; -use crate::common::{fetch_user_details, make_client, make_network, make_test_client, setup_pool}; +use super::common::{fetch_user_details, make_client, make_network, make_test_client, setup_pool}; #[sqlx::test] async fn test_authenticate(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/webhook.rs b/crates/defguard_core/tests/integration/api/webhook.rs similarity index 98% rename from crates/defguard_core/tests/integration/webhook.rs rename to crates/defguard_core/tests/integration/api/webhook.rs index 67dab616ac..326592f3e0 100644 --- a/crates/defguard_core/tests/integration/webhook.rs +++ b/crates/defguard_core/tests/integration/api/webhook.rs @@ -5,7 +5,7 @@ use defguard_core::{ use reqwest::StatusCode; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{make_client, setup_pool}; +use super::common::{make_client, setup_pool}; #[sqlx::test] async fn test_webhooks(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/wireguard.rs b/crates/defguard_core/tests/integration/api/wireguard.rs similarity index 99% rename from crates/defguard_core/tests/integration/wireguard.rs rename to crates/defguard_core/tests/integration/api/wireguard.rs index ab6b41ec1d..becf937611 100644 --- a/crates/defguard_core/tests/integration/wireguard.rs +++ b/crates/defguard_core/tests/integration/api/wireguard.rs @@ -24,7 +24,7 @@ use reqwest::StatusCode; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{ +use super::common::{ authenticate_admin, exceed_enterprise_limits, make_network, make_test_client, setup_pool, }; diff --git a/crates/defguard_core/tests/integration/wireguard_network_allowed_groups.rs b/crates/defguard_core/tests/integration/api/wireguard_network_allowed_groups.rs similarity index 99% rename from crates/defguard_core/tests/integration/wireguard_network_allowed_groups.rs rename to crates/defguard_core/tests/integration/api/wireguard_network_allowed_groups.rs index 16288fa64f..51ecb04207 100644 --- a/crates/defguard_core/tests/integration/wireguard_network_allowed_groups.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_allowed_groups.rs @@ -14,7 +14,7 @@ use sqlx::{ postgres::{PgConnectOptions, PgPoolOptions}, }; -use crate::common::{fetch_user_details, make_test_client, setup_pool}; +use super::common::{fetch_user_details, make_test_client, setup_pool}; // setup user groups, test users and devices async fn setup_test_users(pool: &PgPool) -> (Vec>, Vec>) { diff --git a/crates/defguard_core/tests/integration/wireguard_network_devices.rs b/crates/defguard_core/tests/integration/api/wireguard_network_devices.rs similarity index 99% rename from crates/defguard_core/tests/integration/wireguard_network_devices.rs rename to crates/defguard_core/tests/integration/api/wireguard_network_devices.rs index 1867226b06..7ae1d506da 100644 --- a/crates/defguard_core/tests/integration/wireguard_network_devices.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_devices.rs @@ -11,7 +11,7 @@ use serde::Deserialize; use serde_json::{Value, json}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{make_test_client, setup_pool}; +use super::common::{make_test_client, setup_pool}; fn make_network() -> Value { json!({ diff --git a/crates/defguard_core/tests/integration/wireguard_network_import.rs b/crates/defguard_core/tests/integration/api/wireguard_network_import.rs similarity index 99% rename from crates/defguard_core/tests/integration/wireguard_network_import.rs rename to crates/defguard_core/tests/integration/api/wireguard_network_import.rs index 59217fb90f..aad4c8d62c 100644 --- a/crates/defguard_core/tests/integration/wireguard_network_import.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_import.rs @@ -18,7 +18,7 @@ use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use tokio::sync::broadcast::error::TryRecvError; -use crate::common::{fetch_user_details, make_test_client, setup_pool}; +use super::common::{fetch_user_details, make_test_client, setup_pool}; #[sqlx::test] async fn test_config_import(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/wireguard_network_stats.rs b/crates/defguard_core/tests/integration/api/wireguard_network_stats.rs similarity index 99% rename from crates/defguard_core/tests/integration/wireguard_network_stats.rs rename to crates/defguard_core/tests/integration/api/wireguard_network_stats.rs index 5efb4b5feb..8a02b5c994 100644 --- a/crates/defguard_core/tests/integration/wireguard_network_stats.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_stats.rs @@ -18,7 +18,7 @@ use serde::Deserialize; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{make_network, make_test_client, setup_pool}; +use super::common::{make_network, make_test_client, setup_pool}; static DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:00Z"; diff --git a/crates/defguard_core/tests/integration/worker.rs b/crates/defguard_core/tests/integration/api/worker.rs similarity index 99% rename from crates/defguard_core/tests/integration/worker.rs rename to crates/defguard_core/tests/integration/api/worker.rs index 7243311ff6..88833a892d 100644 --- a/crates/defguard_core/tests/integration/worker.rs +++ b/crates/defguard_core/tests/integration/api/worker.rs @@ -8,7 +8,7 @@ use defguard_core::{ use reqwest::StatusCode; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use crate::common::{make_client_with_state, setup_pool}; +use super::common::{make_client_with_state, setup_pool}; #[sqlx::test] async fn test_scheduling_worker_jobs(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/tests/integration/common.rs b/crates/defguard_core/tests/integration/common.rs new file mode 100644 index 0000000000..d89859fc9c --- /dev/null +++ b/crates/defguard_core/tests/integration/common.rs @@ -0,0 +1,34 @@ +use std::str::FromStr; + +use defguard_core::{SERVER_CONFIG, config::DefGuardConfig, db::User}; +use reqwest::Url; +use secrecy::ExposeSecret; +use sqlx::PgPool; + +/// Allows overriding the default DefGuard URL for tests, as during the tests, the server has a random port, making the URL unpredictable beforehand. +// TODO: Allow customizing the whole config, not just the URL +pub(crate) fn init_config(custom_defguard_url: Option<&str>) -> DefGuardConfig { + let url = custom_defguard_url.unwrap_or("http://localhost:8000"); + let mut config = DefGuardConfig::new_test_config(); + config.url = Url::from_str(url).unwrap(); + let _ = SERVER_CONFIG.set(config.clone()); + config +} + +pub(crate) async fn initialize_users(pool: &PgPool, config: &DefGuardConfig) { + User::init_admin_user(pool, config.default_admin_password.expose_secret()) + .await + .unwrap(); + + User::new( + "hpotter", + Some("pass123"), + "Potter", + "Harry", + "h.potter@hogwart.edu.uk", + None, + ) + .save(pool) + .await + .unwrap(); +} diff --git a/crates/defguard_core/tests/integration/grpc/common/mock_gateway.rs b/crates/defguard_core/tests/integration/grpc/common/mock_gateway.rs new file mode 100644 index 0000000000..3414571036 --- /dev/null +++ b/crates/defguard_core/tests/integration/grpc/common/mock_gateway.rs @@ -0,0 +1,140 @@ +use std::time::Duration; + +use defguard_core::grpc::proto::gateway::{ + Configuration, ConfigurationRequest, StatsUpdate, Update, + gateway_service_client::GatewayServiceClient, +}; +use tokio::{ + sync::mpsc::{UnboundedSender, unbounded_channel}, + task::JoinHandle, + time::timeout, +}; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tonic::{Request, Response, Status, Streaming, metadata::MetadataValue, transport::Channel}; + +pub(crate) struct MockGateway { + client: GatewayServiceClient, + auth_token: Option, + hostname: Option, + stats_update_thread_handle: Option>, + updates_stream: Option>, +} + +impl Drop for MockGateway { + fn drop(&mut self) { + if let Some(handle) = &self.stats_update_thread_handle { + handle.abort(); + } + } +} + +impl MockGateway { + #[must_use] + pub(crate) async fn new(client_channel: Channel) -> Self { + let client = GatewayServiceClient::new(client_channel); + + Self { + client, + auth_token: None, + hostname: None, + stats_update_thread_handle: None, + updates_stream: None, + } + } + + // Add required authorization and hostname headers to gRPC requests + fn add_request_metadata(&self, request: &mut Request) { + // add authorization token + if let Some(token) = &self.auth_token { + request.metadata_mut().insert( + "authorization", + MetadataValue::try_from(token).expect("failed to convert token into metadata"), + ); + }; + + // add gateway hostname + if let Some(hostname) = &self.hostname { + request.metadata_mut().insert( + "hostname", + MetadataValue::try_from(hostname) + .expect("failed to convert hostname into metadata"), + ); + }; + } + + // Fetch gateway config from core + pub(crate) async fn get_gateway_config(&mut self) -> Result, Status> { + let mut request = Request::new(ConfigurationRequest { + name: self.hostname.clone(), + }); + + self.add_request_metadata(&mut request); + + self.client.config(request).await + } + + pub(crate) async fn connect_to_updates_stream(&mut self) { + let mut request = Request::new(()); + + self.add_request_metadata(&mut request); + + let updates_stream = self.client.updates(request).await.unwrap().into_inner(); + + self.updates_stream = Some(updates_stream); + } + + pub(crate) fn disconnect_from_updates_stream(&mut self) { + self.updates_stream = None; + } + + #[must_use] + pub(crate) async fn receive_next_update(&mut self) -> Option { + match &mut self.updates_stream { + Some(stream) => match timeout(Duration::from_millis(100), stream.message()).await { + Ok(result) => result.expect("failed to reveive update message"), + Err(_) => None, + }, + None => None, + } + } + + // Connect to interface stats update endpoint + // and return a tx which can be used to send stats updates to test gRPC server + #[must_use] + pub(crate) async fn setup_stats_update_stream(&mut self) -> UnboundedSender { + let (tx, rx) = unbounded_channel(); + + let mut request = Request::new(UnboundedReceiverStream::new(rx)); + + self.add_request_metadata(&mut request); + + let mut client = self.client.clone(); + let task_handle = tokio::spawn(async move { + client.stats(request).await.expect("stats stream closed"); + }); + + self.stats_update_thread_handle = Some(task_handle); + + tx + } + + pub(crate) fn set_token(&mut self, token: &str) { + self.auth_token = Some(token.into()) + } + + pub(crate) fn clear_token(&mut self) { + self.auth_token = None; + } + + pub(crate) fn set_hostname(&mut self, hostname: &str) { + self.hostname = Some(hostname.into()) + } + + pub(crate) fn clear_hostname(&mut self) { + self.hostname = None; + } + + pub(crate) fn hostname(&self) -> String { + self.hostname.clone().unwrap_or_default() + } +} diff --git a/crates/defguard_core/tests/integration/grpc/common/mod.rs b/crates/defguard_core/tests/integration/grpc/common/mod.rs new file mode 100644 index 0000000000..25ad6e38ff --- /dev/null +++ b/crates/defguard_core/tests/integration/grpc/common/mod.rs @@ -0,0 +1,174 @@ +use std::sync::{Arc, Mutex}; + +use axum::http::Uri; +use defguard_core::{ + auth::failed_login::FailedLoginMap, + db::{AppEvent, GatewayEvent, models::settings::initialize_current_settings}, + enterprise::license::{License, set_cached_license}, + events::GrpcEvent, + grpc::{GatewayMap, WorkerState, build_grpc_service_router, gateway::client_state::ClientMap}, + mail::Mail, +}; +use hyper_util::rt::TokioIo; +use sqlx::PgPool; +use tokio::{ + io::DuplexStream, + sync::{ + broadcast::{self, Sender}, + mpsc::{UnboundedReceiver, unbounded_channel}, + }, + task::JoinHandle, +}; +use tonic::transport::{Channel, Endpoint, Server, server::Router}; +use tower::service_fn; + +use crate::common::{init_config, initialize_users}; + +pub mod mock_gateway; + +pub struct TestGrpcServer { + grpc_server_task_handle: JoinHandle<()>, + pub grpc_event_rx: UnboundedReceiver, + wireguard_tx: Sender, + gateway_state: Arc>, + client_state: Arc>, + pub client_channel: Channel, +} + +impl TestGrpcServer { + #[must_use] + pub async fn new( + server_stream: DuplexStream, + grpc_router: Router, + grpc_event_rx: UnboundedReceiver, + wireguard_tx: Sender, + gateway_state: Arc>, + client_state: Arc>, + client_channel: Channel, + ) -> Self { + // spawn test gRPC server + let grpc_server_task_handle = tokio::spawn(async move { + grpc_router + .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server_stream))) + .await + .map_err(|err| eprintln!("Unexpected test gRPC server error: {err}")) + .unwrap() + }); + + Self { + grpc_server_task_handle, + grpc_event_rx, + wireguard_tx, + gateway_state, + client_state, + client_channel, + } + } + + pub fn get_gateway_map(&self) -> std::sync::MutexGuard<'_, GatewayMap> { + self.gateway_state + .lock() + .expect("failed to acquire lock on gateway state") + } + + pub fn get_client_map(&self) -> std::sync::MutexGuard<'_, ClientMap> { + self.client_state + .lock() + .expect("failed to acquire lock on client state") + } + + pub fn send_wireguard_event(&self, event: GatewayEvent) { + self.wireguard_tx + .send(event) + .expect("failed to send gateway event"); + } +} + +impl Drop for TestGrpcServer { + fn drop(&mut self) { + // explicitly stop spawned gRPC server task + self.grpc_server_task_handle.abort(); + } +} + +pub(crate) async fn create_client_channel(client_stream: DuplexStream) -> Channel { + // Move client to an option so we can _move_ the inner value + // on the first attempt to connect. All other attempts will fail. + // reference: https://github.com/hyperium/tonic/blob/master/examples/src/mock/mock.rs#L31 + let mut client = Some(client_stream); + Endpoint::try_from("http://[::]:50051") + .expect("Failed to create channel") + .connect_with_connector(service_fn(move |_: Uri| { + let client = client.take(); + + async move { + if let Some(client) = client { + Ok(TokioIo::new(client)) + } else { + Err(std::io::Error::other("Client already taken")) + } + } + })) + .await + .expect("Failed to create client channel") +} + +pub(crate) async fn make_grpc_test_server(pool: &PgPool) -> TestGrpcServer { + // create communication channel for clients + let (client_stream, server_stream) = tokio::io::duplex(1024); + let client_channel = create_client_channel(client_stream).await; + + // setup helper structs + let (grpc_event_tx, grpc_event_rx) = unbounded_channel::(); + let (app_event_tx, _app_event_rx) = unbounded_channel::(); + 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 gateway_state = Arc::new(Mutex::new(GatewayMap::new())); + let client_state = Arc::new(Mutex::new(ClientMap::new())); + + let failed_logins = FailedLoginMap::new(); + let failed_logins = Arc::new(Mutex::new(failed_logins)); + + let config = init_config(None); + initialize_users(pool, &config).await; + initialize_current_settings(pool) + .await + .expect("Could not initialize settings"); + + let license = License::new( + "test_customer".to_string(), + false, + // Permanent license + None, + None, + ); + + set_cached_license(Some(license)); + let server = Server::builder(); + + let grpc_router = build_grpc_service_router( + server, + pool.clone(), + worker_state, + gateway_state.clone(), + client_state.clone(), + wg_tx.clone(), + mail_tx, + failed_logins, + grpc_event_tx, + ) + .await + .unwrap(); + + TestGrpcServer::new( + server_stream, + grpc_router, + grpc_event_rx, + wg_tx, + gateway_state, + client_state, + client_channel, + ) + .await +} diff --git a/crates/defguard_core/tests/integration/grpc/gateway.rs b/crates/defguard_core/tests/integration/grpc/gateway.rs new file mode 100644 index 0000000000..e174c968be --- /dev/null +++ b/crates/defguard_core/tests/integration/grpc/gateway.rs @@ -0,0 +1,496 @@ +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::Duration, +}; + +use chrono::{Days, Utc}; +use claims::{assert_err_eq, assert_matches}; +use defguard_core::{ + db::{ + Device, Id, NoId, User, WireguardNetwork, + models::{ + device::DeviceType, wireguard::LocationMfaMode, + wireguard_peer_stats::WireguardPeerStats, + }, + setup_pool, + }, + enterprise::{license::set_cached_license, limits::update_counts}, + events::GrpcEvent, + grpc::{ + gateway::{Configuration, Update, update}, + proto::{ + enterprise::firewall::FirewallPolicy, + gateway::{PeerStats, StatsUpdate, stats_update::Payload}, + }, + }, +}; +use sqlx::{ + PgPool, + postgres::{PgConnectOptions, PgPoolOptions}, +}; +use tokio::{sync::mpsc::error::TryRecvError, time::sleep}; +use tonic::Code; + +use crate::grpc::common::{TestGrpcServer, make_grpc_test_server, mock_gateway::MockGateway}; + +async fn setup_test_server( + pool: PgPool, +) -> (TestGrpcServer, MockGateway, WireguardNetwork, User) { + let test_server = make_grpc_test_server(&pool).await; + + // setup mock gateway + let mut gateway = MockGateway::new(test_server.client_channel.clone()).await; + + // create a test location + let location = WireguardNetwork::new( + "test location".to_string(), + Vec::new(), + 1000, + "endpoint1".to_string(), + None, + Vec::new(), + 100, + 100, + false, + false, + LocationMfaMode::Disabled, + ) + .save(&pool) + .await + .unwrap(); + + // set auth token for gateway + let token = location + .generate_gateway_token() + .expect("failed to generate gateway token"); + gateway.set_token(&token); + + // set hostname for gateway + gateway.set_hostname("test_gateway"); + + // get test user + let test_user = User::find_by_username(&pool, "hpotter") + .await + .unwrap() + .unwrap(); + + (test_server, gateway, location, test_user) +} + +#[sqlx::test] +async fn test_gateway_authorization(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (_test_server, mut gateway, test_location, _test_user) = setup_test_server(pool).await; + + // remove auth token + gateway.clear_token(); + + // make a request without auth token + let response = gateway.get_gateway_config().await; + + // check that response code is `Code::Unauthenticated` + assert!(response.is_err()); + let status = response.err().unwrap(); + assert_eq!(status.code(), Code::Unauthenticated); + + // set invalid token and check again + gateway.set_token("invalid_token"); + let response = gateway.get_gateway_config().await; + assert!(response.is_err()); + let status = response.err().unwrap(); + assert_eq!(status.code(), Code::Unauthenticated); + + // set valid token and retry + let token = test_location.generate_gateway_token().unwrap(); + gateway.set_token(&token); + let response = gateway.get_gateway_config().await; + assert!(response.is_ok()); +} + +#[sqlx::test] +async fn test_gateway_hostname_is_required(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (_test_server, mut gateway, _test_location, _test_user) = setup_test_server(pool).await; + + // remove hostname + gateway.clear_hostname(); + + // make a request without hostname + let response = gateway.get_gateway_config().await; + + // check that response code is `Code::Internal` + assert!(response.is_err()); + let status = response.err().unwrap(); + assert_eq!(status.code(), Code::Internal); + + // set hostname and retry + gateway.set_hostname("hostname"); + let response = gateway.get_gateway_config().await; + assert!(response.is_ok()); +} + +#[sqlx::test] +async fn test_gateway_status(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (test_server, mut gateway, test_location, _test_user) = setup_test_server(pool).await; + + // initial gateway map is empty + { + let gateway_map = test_server.get_gateway_map(); + assert!(gateway_map.is_empty()) + } + + // gateway request initial config + // it should be added to status map as disconnected + let response = gateway.get_gateway_config().await; + assert!(response.is_ok()); + { + let gateway_map = test_server.get_gateway_map(); + let location_gateways = gateway_map.get_network_gateway_status(test_location.id); + assert_eq!(location_gateways.len(), 1); + let gateway_state = location_gateways.first().unwrap(); + assert!(!gateway_state.connected); + assert!(gateway_state.connected_at.is_none()); + assert!(gateway_state.disconnected_at.is_none()); + assert_eq!(gateway_state.hostname, gateway.hostname()); + } + + // gateway connects to updates stream + // it should be marked as connected + gateway.connect_to_updates_stream().await; + { + let gateway_map = test_server.get_gateway_map(); + let location_gateways = gateway_map.get_network_gateway_status(test_location.id); + assert_eq!(location_gateways.len(), 1); + let gateway_state = location_gateways.first().unwrap(); + assert!(gateway_state.connected); + assert!(gateway_state.connected_at.is_some()); + assert!(gateway_state.disconnected_at.is_none()); + assert_eq!(gateway_state.hostname, gateway.hostname()); + } + + // gateway disconnect from updates stream + // it should be marked as disconnected + gateway.disconnect_from_updates_stream(); + // wait for the background thread to handle the disconnect + sleep(Duration::from_millis(100)).await; + + { + let gateway_map = test_server.get_gateway_map(); + let location_gateways = gateway_map.get_network_gateway_status(test_location.id); + assert_eq!(location_gateways.len(), 1); + let gateway_state = location_gateways.first().unwrap(); + assert!(!gateway_state.connected); + assert!(gateway_state.connected_at.is_some()); + assert!(gateway_state.disconnected_at.is_some()); + assert_eq!(gateway_state.hostname, gateway.hostname()); + } +} + +#[sqlx::test] +async fn test_vpn_client_connected(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (mut test_server, mut gateway, test_location, test_user) = + setup_test_server(pool.clone()).await; + + // initial client map is empty + { + let client_map = test_server.get_client_map(); + assert!(client_map.is_empty()) + } + + // connect stats stream + let stats_tx = gateway.setup_stats_update_stream().await; + let mut update_id = 1; + + // add user device + let device_pubkey = "wYOt6ImBaQ3BEMQ3Xf5P5fTnbqwOvjcqYkkSBt+1xOg="; + let test_device = Device::new( + "test device".into(), + device_pubkey.into(), + test_user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + // send stats update for existing device with old handshake + // and verify no gRPC event is emitted + stats_tx + .send(StatsUpdate { + id: update_id, + payload: Some(Payload::PeerStats(PeerStats { + public_key: device_pubkey.into(), + endpoint: "1.2.3.4:1234".into(), + latest_handshake: 0, + ..Default::default() + })), + }) + .expect("failed to send stats update"); + + assert_err_eq!(test_server.grpc_event_rx.try_recv(), TryRecvError::Empty); + + // send stats update with current handshake + update_id += 1; + stats_tx + .send(StatsUpdate { + id: update_id, + payload: Some(Payload::PeerStats(PeerStats { + public_key: device_pubkey.into(), + endpoint: "1.2.3.4:1234".into(), + latest_handshake: Utc::now().timestamp() as u64, + ..Default::default() + })), + }) + .expect("failed to send stats update"); + + // wait for event to be emitted + sleep(Duration::from_millis(100)).await; + let grpc_event = test_server + .grpc_event_rx + .try_recv() + .expect("failed to receive gRPC event"); + + assert_matches!( + grpc_event, + GrpcEvent::ClientConnected { + context: _, + location, + device + } if ((location.id == test_location.id) & (device.id == test_device.id)) + ); +} + +#[sqlx::test] +async fn test_vpn_client_disconnected(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (mut test_server, mut gateway, test_location, test_user) = + setup_test_server(pool.clone()).await; + + // add user device + let device_pubkey = "wYOt6ImBaQ3BEMQ3Xf5P5fTnbqwOvjcqYkkSBt+1xOg="; + let test_device = Device::new( + "test device".into(), + device_pubkey.into(), + test_user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + // insert device into client map with an old handshake + { + let mut client_map = test_server.get_client_map(); + let now = Utc::now().naive_utc(); + let stats = WireguardPeerStats { + id: NoId, + device_id: test_device.id, + collected_at: now, + network: test_location.id, + endpoint: None, + upload: 0, + download: 0, + latest_handshake: now.checked_sub_days(Days::new(1)).unwrap(), + allowed_ips: None, + }; + client_map + .connect_vpn_client( + test_location.id, + &gateway.hostname(), + device_pubkey, + &test_device, + &test_user, + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), + &stats, + ) + .expect("failed to insert connected client"); + } + + // connect stats stream + let stats_tx = gateway.setup_stats_update_stream().await; + let mut update_id = 1; + + // send stats update with old handshake + update_id += 1; + stats_tx + .send(StatsUpdate { + id: update_id, + payload: Some(Payload::PeerStats(PeerStats { + public_key: device_pubkey.into(), + endpoint: "1.2.3.4:1234".into(), + latest_handshake: 0, + ..Default::default() + })), + }) + .expect("failed to send stats update"); + + // wait for event to be emitted + sleep(Duration::from_millis(100)).await; + let grpc_event = test_server + .grpc_event_rx + .try_recv() + .expect("failed to receive gRPC event"); + + assert_matches!( + grpc_event, + GrpcEvent::ClientDisconnected { + context: _, + location, + device + } if ((location.id == test_location.id) & (device.id == test_device.id)) + ); +} + +#[sqlx::test] +async fn test_gateway_update_routing(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (test_server, mut gateway_1, test_location, _test_user) = + setup_test_server(pool.clone()).await; + + // setup another test location & gateway + let test_location_2 = WireguardNetwork::new( + "test location 2".to_string(), + Vec::new(), + 1000, + "endpoint2".to_string(), + None, + Vec::new(), + 100, + 100, + false, + false, + LocationMfaMode::Disabled, + ) + .save(&pool) + .await + .unwrap(); + let mut gateway_2 = MockGateway::new(test_server.client_channel.clone()).await; + + // set auth token for gateway + let token = test_location_2 + .generate_gateway_token() + .expect("failed to generate gateway token"); + gateway_2.set_token(&token); + + // set hostname for gateway + gateway_2.set_hostname("test_gateway_2"); + + // register gateways with core + let _config_1 = gateway_1.get_gateway_config().await; + let _config_2 = gateway_2.get_gateway_config().await; + + // connect gateways to the updates stream + gateway_1.connect_to_updates_stream().await; + gateway_2.connect_to_updates_stream().await; + + // send update for location 1 + test_server.send_wireguard_event(defguard_core::db::GatewayEvent::NetworkDeleted( + test_location.id, + "network name".into(), + )); + + // only one gateway should receive this update + assert!(gateway_2.receive_next_update().await.is_none()); + let update = gateway_1.receive_next_update().await.unwrap(); + let expected_update = Update { + update_type: 2, + update: Some(update::Update::Network(Configuration { + name: "network name".into(), + prvkey: String::new(), + addresses: Vec::new(), + port: 0, + peers: Vec::new(), + firewall_config: None, + })), + }; + assert_eq!(update, expected_update); + + // send update for location 2 + test_server.send_wireguard_event(defguard_core::db::GatewayEvent::NetworkDeleted( + test_location_2.id, + "network name 2".into(), + )); + + // only one gateway should receive this update + assert!(gateway_1.receive_next_update().await.is_none()); + let update = gateway_2.receive_next_update().await.unwrap(); + let expected_update = Update { + update_type: 2, + update: Some(update::Update::Network(Configuration { + name: "network name 2".into(), + prvkey: String::new(), + addresses: Vec::new(), + port: 0, + peers: Vec::new(), + firewall_config: None, + })), + }; + assert_eq!(update, expected_update); + + // send update for location which does not exist + test_server.send_wireguard_event(defguard_core::db::GatewayEvent::NetworkDeleted( + 1234, + "does not exist".into(), + )); + + // no gateway should receive this update + assert!(gateway_1.receive_next_update().await.is_none()); + assert!(gateway_2.receive_next_update().await.is_none()); +} + +#[sqlx::test] +async fn test_gateway_config(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (_test_server, mut gateway, mut test_location, _test_user) = + setup_test_server(pool.clone()).await; + + // get gateway config + let config = gateway.get_gateway_config().await.unwrap().into_inner(); + + assert_eq!(config.name, test_location.name); + assert!(config.firewall_config.is_none()); + + // enable ACL for test location + test_location.acl_enabled = true; + test_location + .save(&pool) + .await + .expect("failed to update location"); + + // get gateway config + let config = gateway.get_gateway_config().await.unwrap().into_inner(); + assert!(config.firewall_config.is_some()); + assert_eq!( + config.firewall_config.unwrap().default_policy == i32::from(FirewallPolicy::Allow), + test_location.acl_default_allow + ); + + // unset the license and create another location to exceed limits and disable enterprise features + set_cached_license(None); + let _test_location_2 = WireguardNetwork::new( + "test location 2".to_string(), + Vec::new(), + 1000, + "endpoint2".to_string(), + None, + Vec::new(), + 100, + 100, + false, + false, + LocationMfaMode::Disabled, + ) + .save(&pool) + .await + .unwrap(); + update_counts(&pool).await.unwrap(); + + let config = gateway.get_gateway_config().await.unwrap().into_inner(); + assert!(config.firewall_config.is_none()); +} diff --git a/crates/defguard_core/tests/integration/grpc/mod.rs b/crates/defguard_core/tests/integration/grpc/mod.rs new file mode 100644 index 0000000000..5b53a1b0d6 --- /dev/null +++ b/crates/defguard_core/tests/integration/grpc/mod.rs @@ -0,0 +1,2 @@ +mod common; +mod gateway; diff --git a/crates/defguard_core/tests/integration/main.rs b/crates/defguard_core/tests/integration/main.rs index 4891501678..f85d8d0fa3 100644 --- a/crates/defguard_core/tests/integration/main.rs +++ b/crates/defguard_core/tests/integration/main.rs @@ -1,21 +1,3 @@ -mod acl; -mod api_tokens; -mod auth; +mod api; mod common; -mod enrollment; -mod enterprise_settings; -mod forward_auth; -mod group; -mod oauth; -mod openid; -mod openid_login; -mod settings; -mod snat; -mod user; -mod webhook; -mod wireguard; -mod wireguard_network_allowed_groups; -mod wireguard_network_devices; -mod wireguard_network_import; -mod wireguard_network_stats; -mod worker; +mod grpc; diff --git a/flake.lock b/flake.lock index f650d8e58d..c5eade4b1d 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755186698, - "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", + "lastModified": 1755615617, + "narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", + "rev": "20075955deac2583bb12f07151c2df830ef346b4", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1755571033, - "narHash": "sha256-V8gmZBfMiFGCyGJQx/yO81LFJ4d/I5Jxs2id96rLxrM=", + "lastModified": 1756089517, + "narHash": "sha256-KGinVKturJFPrRebgvyUB1BUNqf1y9FN+tSJaTPlnFE=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "95487740bb7ac11553445e9249041a6fa4b5eccf", + "rev": "44774c8c83cd392c50914f86e1ff75ef8619f1cd", "type": "github" }, "original": { diff --git a/web/package.json b/web/package.json index c0175bdd97..dd956aa6b6 100644 --- a/web/package.json +++ b/web/package.json @@ -43,7 +43,7 @@ ] }, "dependencies": { - "@floating-ui/react": "^0.27.15", + "@floating-ui/react": "^0.27.16", "@github/webauthn-json": "^2.1.1", "@hookform/resolvers": "^5.2.1", "@react-hook/resize-observer": "^2.0.2", @@ -125,13 +125,13 @@ "@types/lodash-es": "^4.17.12", "@types/node": "^24.3.0", "@types/qs": "^6.14.0", - "@types/react": "^19.1.10", + "@types/react": "^19.1.11", "@types/react-dom": "^19.1.7", "@types/react-router-dom": "^5.3.3", "@types/react-window": "^1.8.8", "@vitejs/plugin-react-swc": "^4.0.1", "autoprefixer": "^10.4.21", - "concurrently": "^9.2.0", + "concurrently": "^9.2.1", "dotenv": "^17.2.1", "esbuild": "^0.25.9", "globals": "^16.3.0", @@ -144,4 +144,4 @@ "vite": "^7.1.3", "vite-plugin-package-version": "^1.1.0" } -} +} \ No newline at end of file diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 264553317b..9dd237555b 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@floating-ui/react': - specifier: ^0.27.15 - version: 0.27.15(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^0.27.16 + version: 0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@github/webauthn-json': specifier: ^2.1.1 version: 2.1.1 @@ -91,7 +91,7 @@ importers: version: 5.0.0 html-react-parser: specifier: ^5.2.6 - version: 5.2.6(@types/react@19.1.10)(react@19.1.1) + version: 5.2.6(@types/react@19.1.11)(react@19.1.1) humanize-duration: specifier: ^3.33.0 version: 3.33.0 @@ -109,7 +109,7 @@ importers: version: 4.17.21 merge-refs: specifier: ^2.0.0 - version: 2.0.0(@types/react@19.1.10) + version: 2.0.0(@types/react@19.1.11) millify: specifier: ^6.1.0 version: 6.1.0 @@ -157,7 +157,7 @@ importers: version: 3.5.0(react@19.1.1) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@19.1.10)(react@19.1.1) + version: 10.1.0(@types/react@19.1.11)(react@19.1.1) react-qr-code: specifier: ^2.0.18 version: 2.0.18(react@19.1.1) @@ -181,7 +181,7 @@ importers: version: 1.8.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1) recharts: specifier: ^3.1.2 - version: 3.1.2(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react-is@19.1.1)(react@19.1.1)(redux@5.0.1) + version: 3.1.2(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react-is@19.1.1)(react@19.1.1)(redux@5.0.1) rehype-external-links: specifier: ^3.0.0 version: 3.0.0 @@ -211,7 +211,7 @@ importers: version: 3.25.76 zustand: specifier: ^5.0.8 - version: 5.0.8(@types/react@19.1.10)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + version: 5.0.8(@types/react@19.1.11)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) devDependencies: '@babel/core': specifier: ^7.28.3 @@ -227,7 +227,7 @@ importers: version: 3.0.4 '@hookform/devtools': specifier: ^4.4.0 - version: 4.4.0(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 4.4.0(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tanstack/react-query-devtools': specifier: ^5.85.5 version: 5.85.5(@tanstack/react-query@5.85.5(react@19.1.1))(react@19.1.1) @@ -250,11 +250,11 @@ importers: specifier: ^6.14.0 version: 6.14.0 '@types/react': - specifier: ^19.1.10 - version: 19.1.10 + specifier: ^19.1.11 + version: 19.1.11 '@types/react-dom': specifier: ^19.1.7 - version: 19.1.7(@types/react@19.1.10) + version: 19.1.7(@types/react@19.1.11) '@types/react-router-dom': specifier: ^5.3.3 version: 5.3.3 @@ -268,8 +268,8 @@ importers: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) concurrently: - specifier: ^9.2.0 - version: 9.2.0 + specifier: ^9.2.1 + version: 9.2.1 dotenv: specifier: ^17.2.1 version: 17.2.1 @@ -657,17 +657,17 @@ packages: '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.7.3': - resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} - '@floating-ui/react-dom@2.1.5': - resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==} + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.27.15': - resolution: {integrity: sha512-0LGxhBi3BB1DwuSNQAmuaSuertFzNAerlMdPbotjTVnvPtdOs7CkrHLaev5NIXemhzDXNC0tFzuseut7cWA5mw==} + '@floating-ui/react@0.27.16': + resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==} peerDependencies: react: '>=17.0.0' react-dom: '>=17.0.0' @@ -749,103 +749,103 @@ packages: '@rolldown/pluginutils@1.0.0-beta.32': resolution: {integrity: sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==} - '@rollup/rollup-android-arm-eabi@4.46.4': - resolution: {integrity: sha512-B2wfzCJ+ps/OBzRjeds7DlJumCU3rXMxJJS1vzURyj7+KBHGONm7c9q1TfdBl4vCuNMkDvARn3PBl2wZzuR5mw==} + '@rollup/rollup-android-arm-eabi@4.48.1': + resolution: {integrity: sha512-rGmb8qoG/zdmKoYELCBwu7vt+9HxZ7Koos3pD0+sH5fR3u3Wb/jGcpnqxcnWsPEKDUyzeLSqksN8LJtgXjqBYw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.46.4': - resolution: {integrity: sha512-FGJYXvYdn8Bs6lAlBZYT5n+4x0ciEp4cmttsvKAZc/c8/JiPaQK8u0c/86vKX8lA7OY/+37lIQSe0YoAImvBAA==} + '@rollup/rollup-android-arm64@4.48.1': + resolution: {integrity: sha512-4e9WtTxrk3gu1DFE+imNJr4WsL13nWbD/Y6wQcyku5qadlKHY3OQ3LJ/INrrjngv2BJIHnIzbqMk1GTAC2P8yQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.46.4': - resolution: {integrity: sha512-/9qwE/BM7ATw/W/OFEMTm3dmywbJyLQb4f4v5nmOjgYxPIGpw7HaxRi6LnD4Pjn/q7k55FGeHe1/OD02w63apA==} + '@rollup/rollup-darwin-arm64@4.48.1': + resolution: {integrity: sha512-+XjmyChHfc4TSs6WUQGmVf7Hkg8ferMAE2aNYYWjiLzAS/T62uOsdfnqv+GHRjq7rKRnYh4mwWb4Hz7h/alp8A==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.46.4': - resolution: {integrity: sha512-QkWfNbeRuzFnv2d0aPlrzcA3Ebq2mE8kX/5Pl7VdRShbPBjSnom7dbT8E3Jmhxo2RL784hyqGvR5KHavCJQciw==} + '@rollup/rollup-darwin-x64@4.48.1': + resolution: {integrity: sha512-upGEY7Ftw8M6BAJyGwnwMw91rSqXTcOKZnnveKrVWsMTF8/k5mleKSuh7D4v4IV1pLxKAk3Tbs0Lo9qYmii5mQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.4': - resolution: {integrity: sha512-+ToyOMYnSfV8D+ckxO6NthPln/PDNp1P6INcNypfZ7muLmEvPKXqduUiD8DlJpMMT8LxHcE5W0dK9kXfJke9Zw==} + '@rollup/rollup-freebsd-arm64@4.48.1': + resolution: {integrity: sha512-P9ViWakdoynYFUOZhqq97vBrhuvRLAbN/p2tAVJvhLb8SvN7rbBnJQcBu8e/rQts42pXGLVhfsAP0k9KXWa3nQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.46.4': - resolution: {integrity: sha512-cGT6ey/W+sje6zywbLiqmkfkO210FgRz7tepWAzzEVgQU8Hn91JJmQWNqs55IuglG8sJdzk7XfNgmGRtcYlo1w==} + '@rollup/rollup-freebsd-x64@4.48.1': + resolution: {integrity: sha512-VLKIwIpnBya5/saccM8JshpbxfyJt0Dsli0PjXozHwbSVaHTvWXJH1bbCwPXxnMzU4zVEfgD1HpW3VQHomi2AQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.46.4': - resolution: {integrity: sha512-9fhTJyOb275w5RofPSl8lpr4jFowd+H4oQKJ9XTYzD1JWgxdZKE8bA6d4npuiMemkecQOcigX01FNZNCYnQBdA==} + '@rollup/rollup-linux-arm-gnueabihf@4.48.1': + resolution: {integrity: sha512-3zEuZsXfKaw8n/yF7t8N6NNdhyFw3s8xJTqjbTDXlipwrEHo4GtIKcMJr5Ed29leLpB9AugtAQpAHW0jvtKKaQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.46.4': - resolution: {integrity: sha512-+6kCIM5Zjvz2HwPl/udgVs07tPMIp1VU2Y0c72ezjOvSvEfAIWsUgpcSDvnC7g9NrjYR6X9bZT92mZZ90TfvXw==} + '@rollup/rollup-linux-arm-musleabihf@4.48.1': + resolution: {integrity: sha512-leo9tOIlKrcBmmEypzunV/2w946JeLbTdDlwEZ7OnnsUyelZ72NMnT4B2vsikSgwQifjnJUbdXzuW4ToN1wV+Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.46.4': - resolution: {integrity: sha512-SWuXdnsayCZL4lXoo6jn0yyAj7TTjWE4NwDVt9s7cmu6poMhtiras5c8h6Ih6Y0Zk6Z+8t/mLumvpdSPTWub2Q==} + '@rollup/rollup-linux-arm64-gnu@4.48.1': + resolution: {integrity: sha512-Vy/WS4z4jEyvnJm+CnPfExIv5sSKqZrUr98h03hpAMbE2aI0aD2wvK6GiSe8Gx2wGp3eD81cYDpLLBqNb2ydwQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.46.4': - resolution: {integrity: sha512-vDknMDqtMhrrroa5kyX6tuC0aRZZlQ+ipDfbXd2YGz5HeV2t8HOl/FDAd2ynhs7Ki5VooWiiZcCtxiZ4IjqZwQ==} + '@rollup/rollup-linux-arm64-musl@4.48.1': + resolution: {integrity: sha512-x5Kzn7XTwIssU9UYqWDB9VpLpfHYuXw5c6bJr4Mzv9kIv242vmJHbI5PJJEnmBYitUIfoMCODDhR7KoZLot2VQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.46.4': - resolution: {integrity: sha512-mCBkjRZWhvjtl/x+Bd4fQkWZT8canStKDxGrHlBiTnZmJnWygGcvBylzLVCZXka4dco5ymkWhZlLwKCGFF4ivw==} + '@rollup/rollup-linux-loongarch64-gnu@4.48.1': + resolution: {integrity: sha512-yzCaBbwkkWt/EcgJOKDUdUpMHjhiZT/eDktOPWvSRpqrVE04p0Nd6EGV4/g7MARXXeOqstflqsKuXVM3H9wOIQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.46.4': - resolution: {integrity: sha512-YMdz2phOTFF+Z66dQfGf0gmeDSi5DJzY5bpZyeg9CPBkV9QDzJ1yFRlmi/j7WWRf3hYIWrOaJj5jsfwgc8GTHQ==} + '@rollup/rollup-linux-ppc64-gnu@4.48.1': + resolution: {integrity: sha512-UK0WzWUjMAJccHIeOpPhPcKBqax7QFg47hwZTp6kiMhQHeOYJeaMwzeRZe1q5IiTKsaLnHu9s6toSYVUlZ2QtQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.46.4': - resolution: {integrity: sha512-r0WKLSfFAK8ucG024v2yiLSJMedoWvk8yWqfNICX28NHDGeu3F/wBf8KG6mclghx4FsLePxJr/9N8rIj1PtCnw==} + '@rollup/rollup-linux-riscv64-gnu@4.48.1': + resolution: {integrity: sha512-3NADEIlt+aCdCbWVZ7D3tBjBX1lHpXxcvrLt/kdXTiBrOds8APTdtk2yRL2GgmnSVeX4YS1JIf0imFujg78vpw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.46.4': - resolution: {integrity: sha512-IaizpPP2UQU3MNyPH1u0Xxbm73D+4OupL0bjo4Hm0496e2wg3zuvoAIhubkD1NGy9fXILEExPQy87mweujEatA==} + '@rollup/rollup-linux-riscv64-musl@4.48.1': + resolution: {integrity: sha512-euuwm/QTXAMOcyiFCcrx0/S2jGvFlKJ2Iro8rsmYL53dlblp3LkUQVFzEidHhvIPPvcIsxDhl2wkBE+I6YVGzA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.46.4': - resolution: {integrity: sha512-aCM29orANR0a8wk896p6UEgIfupReupnmISz6SUwMIwTGaTI8MuKdE0OD2LvEg8ondDyZdMvnaN3bW4nFbATPA==} + '@rollup/rollup-linux-s390x-gnu@4.48.1': + resolution: {integrity: sha512-w8mULUjmPdWLJgmTYJx/W6Qhln1a+yqvgwmGXcQl2vFBkWsKGUBRbtLRuKJUln8Uaimf07zgJNxOhHOvjSQmBQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.46.4': - resolution: {integrity: sha512-0Xj1vZE3cbr/wda8d/m+UeuSL+TDpuozzdD4QaSzu/xSOMK0Su5RhIkF7KVHFQsobemUNHPLEcYllL7ZTCP/Cg==} + '@rollup/rollup-linux-x64-gnu@4.48.1': + resolution: {integrity: sha512-90taWXCWxTbClWuMZD0DKYohY1EovA+W5iytpE89oUPmT5O1HFdf8cuuVIylE6vCbrGdIGv85lVRzTcpTRZ+kA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.46.4': - resolution: {integrity: sha512-kM/orjpolfA5yxsx84kI6bnK47AAZuWxglGKcNmokw2yy9i5eHY5UAjcX45jemTJnfHAWo3/hOoRqEeeTdL5hw==} + '@rollup/rollup-linux-x64-musl@4.48.1': + resolution: {integrity: sha512-2Gu29SkFh1FfTRuN1GR1afMuND2GKzlORQUP3mNMJbqdndOg7gNsa81JnORctazHRokiDzQ5+MLE5XYmZW5VWg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.46.4': - resolution: {integrity: sha512-cNLH4psMEsWKILW0isbpQA2OvjXLbKvnkcJFmqAptPQbtLrobiapBJVj6RoIvg6UXVp5w0wnIfd/Q56cNpF+Ew==} + '@rollup/rollup-win32-arm64-msvc@4.48.1': + resolution: {integrity: sha512-6kQFR1WuAO50bxkIlAVeIYsz3RUx+xymwhTo9j94dJ+kmHe9ly7muH23sdfWduD0BA8pD9/yhonUvAjxGh34jQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.46.4': - resolution: {integrity: sha512-OiEa5lRhiANpv4SfwYVgQ3opYWi/QmPDC5ve21m8G9pf6ZO+aX1g2EEF1/IFaM1xPSP7mK0msTRXlPs6mIagkg==} + '@rollup/rollup-win32-ia32-msvc@4.48.1': + resolution: {integrity: sha512-RUyZZ/mga88lMI3RlXFs4WQ7n3VyU07sPXmMG7/C1NOi8qisUg57Y7LRarqoGoAiopmGmChUhSwfpvQ3H5iGSQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.46.4': - resolution: {integrity: sha512-IKL9mewGZ5UuuX4NQlwOmxPyqielvkAPUS2s1cl6yWjjQvyN3h5JTdVFGD5Jr5xMjRC8setOfGQDVgX8V+dkjg==} + '@rollup/rollup-win32-x64-msvc@4.48.1': + resolution: {integrity: sha512-8a/caCUN4vkTChxkaIJcMtwIVcBhi4X2PQRoT+yCK3qRYaZ7cURrmJFL5Ux9H9RaMIXj9RuihckdmkBX3zZsgg==} cpu: [x64] os: [win32] @@ -884,68 +884,68 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@swc/core-darwin-arm64@1.13.3': - resolution: {integrity: sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==} + '@swc/core-darwin-arm64@1.13.5': + resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.13.3': - resolution: {integrity: sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==} + '@swc/core-darwin-x64@1.13.5': + resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.13.3': - resolution: {integrity: sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==} + '@swc/core-linux-arm-gnueabihf@1.13.5': + resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.13.3': - resolution: {integrity: sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==} + '@swc/core-linux-arm64-gnu@1.13.5': + resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.13.3': - resolution: {integrity: sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==} + '@swc/core-linux-arm64-musl@1.13.5': + resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.13.3': - resolution: {integrity: sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==} + '@swc/core-linux-x64-gnu@1.13.5': + resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.13.3': - resolution: {integrity: sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==} + '@swc/core-linux-x64-musl@1.13.5': + resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.13.3': - resolution: {integrity: sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==} + '@swc/core-win32-arm64-msvc@1.13.5': + resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.13.3': - resolution: {integrity: sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==} + '@swc/core-win32-ia32-msvc@1.13.5': + resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.13.3': - resolution: {integrity: sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==} + '@swc/core-win32-x64-msvc@1.13.5': + resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.13.3': - resolution: {integrity: sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==} + '@swc/core@1.13.5': + resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1077,8 +1077,8 @@ packages: '@types/react-window@1.8.8': resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} - '@types/react@19.1.10': - resolution: {integrity: sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==} + '@types/react@19.1.11': + resolution: {integrity: sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1215,8 +1215,8 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - caniuse-lite@1.0.30001735: - resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + caniuse-lite@1.0.30001737: + resolution: {integrity: sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1295,8 +1295,8 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} - concurrently@9.2.0: - resolution: {integrity: sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==} + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} engines: {node: '>=18'} hasBin: true @@ -1528,8 +1528,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.207: - resolution: {integrity: sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==} + electron-to-chromium@1.5.208: + resolution: {integrity: sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2575,8 +2575,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rollup@4.46.4: - resolution: {integrity: sha512-YbxoxvoqNg9zAmw4+vzh1FkGAiZRK+LhnSrbSrSXMdZYsRPDWoshcSd/pldKRO6lWzv/e9TiJAVQyirYIeSIPQ==} + rollup@4.48.1: + resolution: {integrity: sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3266,7 +3266,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.1.10)(react@19.1.1)': + '@emotion/react@11.14.0(@types/react@19.1.11)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 '@emotion/babel-plugin': 11.13.5 @@ -3278,7 +3278,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.1.1 optionalDependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.11 transitivePeerDependencies: - supports-color @@ -3292,18 +3292,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.10)(react@19.1.1))(@types/react@19.1.10)(react@19.1.1)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.11)(react@19.1.1))(@types/react@19.1.11)(react@19.1.1)': dependencies: '@babel/runtime': 7.28.3 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.1.10)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.11)(react@19.1.1) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) '@emotion/utils': 1.4.2 react: 19.1.1 optionalDependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.11 transitivePeerDependencies: - supports-color @@ -3399,20 +3399,20 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.3': + '@floating-ui/dom@1.7.4': dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react-dom@2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@floating-ui/dom': 1.7.3 + '@floating-ui/dom': 1.7.4 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - '@floating-ui/react@0.27.15(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react@0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@floating-ui/react-dom': 2.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@floating-ui/utils': 0.2.10 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) @@ -3422,10 +3422,10 @@ snapshots: '@github/webauthn-json@2.1.1': {} - '@hookform/devtools@4.4.0(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@hookform/devtools@4.4.0(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@emotion/react': 11.14.0(@types/react@19.1.10)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.10)(react@19.1.1))(@types/react@19.1.10)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.1.11)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.11)(react@19.1.1))(@types/react@19.1.11)(react@19.1.1) '@types/lodash': 4.17.20 little-state-machine: 4.8.1(react@19.1.1) lodash: 4.17.21 @@ -3486,7 +3486,7 @@ snapshots: rxjs: 7.8.2 use-sync-external-store: 1.5.0(react@19.1.1) - '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1))(react@19.1.1)': + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1))(react@19.1.1)': dependencies: '@standard-schema/spec': 1.0.0 '@standard-schema/utils': 0.3.0 @@ -3496,70 +3496,70 @@ snapshots: reselect: 5.1.1 optionalDependencies: react: 19.1.1 - react-redux: 9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1) + react-redux: 9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1) '@remix-run/router@1.23.0': {} '@rolldown/pluginutils@1.0.0-beta.32': {} - '@rollup/rollup-android-arm-eabi@4.46.4': + '@rollup/rollup-android-arm-eabi@4.48.1': optional: true - '@rollup/rollup-android-arm64@4.46.4': + '@rollup/rollup-android-arm64@4.48.1': optional: true - '@rollup/rollup-darwin-arm64@4.46.4': + '@rollup/rollup-darwin-arm64@4.48.1': optional: true - '@rollup/rollup-darwin-x64@4.46.4': + '@rollup/rollup-darwin-x64@4.48.1': optional: true - '@rollup/rollup-freebsd-arm64@4.46.4': + '@rollup/rollup-freebsd-arm64@4.48.1': optional: true - '@rollup/rollup-freebsd-x64@4.46.4': + '@rollup/rollup-freebsd-x64@4.48.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.46.4': + '@rollup/rollup-linux-arm-gnueabihf@4.48.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.46.4': + '@rollup/rollup-linux-arm-musleabihf@4.48.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.46.4': + '@rollup/rollup-linux-arm64-gnu@4.48.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.46.4': + '@rollup/rollup-linux-arm64-musl@4.48.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.4': + '@rollup/rollup-linux-loongarch64-gnu@4.48.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.46.4': + '@rollup/rollup-linux-ppc64-gnu@4.48.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.4': + '@rollup/rollup-linux-riscv64-gnu@4.48.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.46.4': + '@rollup/rollup-linux-riscv64-musl@4.48.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.46.4': + '@rollup/rollup-linux-s390x-gnu@4.48.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.46.4': + '@rollup/rollup-linux-x64-gnu@4.48.1': optional: true - '@rollup/rollup-linux-x64-musl@4.46.4': + '@rollup/rollup-linux-x64-musl@4.48.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.46.4': + '@rollup/rollup-win32-arm64-msvc@4.48.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.46.4': + '@rollup/rollup-win32-ia32-msvc@4.48.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.46.4': + '@rollup/rollup-win32-x64-msvc@4.48.1': optional: true '@rx-state/core@0.1.4(rxjs@7.8.2)': @@ -3597,51 +3597,51 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@swc/core-darwin-arm64@1.13.3': + '@swc/core-darwin-arm64@1.13.5': optional: true - '@swc/core-darwin-x64@1.13.3': + '@swc/core-darwin-x64@1.13.5': optional: true - '@swc/core-linux-arm-gnueabihf@1.13.3': + '@swc/core-linux-arm-gnueabihf@1.13.5': optional: true - '@swc/core-linux-arm64-gnu@1.13.3': + '@swc/core-linux-arm64-gnu@1.13.5': optional: true - '@swc/core-linux-arm64-musl@1.13.3': + '@swc/core-linux-arm64-musl@1.13.5': optional: true - '@swc/core-linux-x64-gnu@1.13.3': + '@swc/core-linux-x64-gnu@1.13.5': optional: true - '@swc/core-linux-x64-musl@1.13.3': + '@swc/core-linux-x64-musl@1.13.5': optional: true - '@swc/core-win32-arm64-msvc@1.13.3': + '@swc/core-win32-arm64-msvc@1.13.5': optional: true - '@swc/core-win32-ia32-msvc@1.13.3': + '@swc/core-win32-ia32-msvc@1.13.5': optional: true - '@swc/core-win32-x64-msvc@1.13.3': + '@swc/core-win32-x64-msvc@1.13.5': optional: true - '@swc/core@1.13.3': + '@swc/core@1.13.5': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.24 optionalDependencies: - '@swc/core-darwin-arm64': 1.13.3 - '@swc/core-darwin-x64': 1.13.3 - '@swc/core-linux-arm-gnueabihf': 1.13.3 - '@swc/core-linux-arm64-gnu': 1.13.3 - '@swc/core-linux-arm64-musl': 1.13.3 - '@swc/core-linux-x64-gnu': 1.13.3 - '@swc/core-linux-x64-musl': 1.13.3 - '@swc/core-win32-arm64-msvc': 1.13.3 - '@swc/core-win32-ia32-msvc': 1.13.3 - '@swc/core-win32-x64-msvc': 1.13.3 + '@swc/core-darwin-arm64': 1.13.5 + '@swc/core-darwin-x64': 1.13.5 + '@swc/core-linux-arm-gnueabihf': 1.13.5 + '@swc/core-linux-arm64-gnu': 1.13.5 + '@swc/core-linux-arm64-musl': 1.13.5 + '@swc/core-linux-x64-gnu': 1.13.5 + '@swc/core-linux-x64-musl': 1.13.5 + '@swc/core-win32-arm64-msvc': 1.13.5 + '@swc/core-win32-ia32-msvc': 1.13.5 + '@swc/core-win32-x64-msvc': 1.13.5 '@swc/counter@0.1.3': {} @@ -3742,26 +3742,26 @@ snapshots: '@types/qs@6.14.0': {} - '@types/react-dom@19.1.7(@types/react@19.1.10)': + '@types/react-dom@19.1.7(@types/react@19.1.11)': dependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.11 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.1.10 + '@types/react': 19.1.11 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.1.10 + '@types/react': 19.1.11 '@types/react-window@1.8.8': dependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.11 - '@types/react@19.1.10': + '@types/react@19.1.11': dependencies: csstype: 3.1.3 @@ -3783,7 +3783,7 @@ snapshots: '@vitejs/plugin-react-swc@4.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.32 - '@swc/core': 1.13.3 + '@swc/core': 1.13.5 vite: 7.1.3(@types/node@24.3.0)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) transitivePeerDependencies: - '@swc/helpers' @@ -3822,7 +3822,7 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.25.3 - caniuse-lite: 1.0.30001735 + caniuse-lite: 1.0.30001737 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -3862,8 +3862,8 @@ snapshots: browserslist@4.25.3: dependencies: - caniuse-lite: 1.0.30001735 - electron-to-chromium: 1.5.207 + caniuse-lite: 1.0.30001737 + electron-to-chromium: 1.5.208 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.3) @@ -3891,7 +3891,7 @@ snapshots: camelcase@5.3.1: {} - caniuse-lite@1.0.30001735: {} + caniuse-lite@1.0.30001737: {} ccount@2.0.1: {} @@ -3983,10 +3983,9 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - concurrently@9.2.0: + concurrently@9.2.1: dependencies: chalk: 4.1.2 - lodash: 4.17.21 rxjs: 7.8.2 shell-quote: 1.8.3 supports-color: 8.1.1 @@ -4241,7 +4240,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.207: {} + electron-to-chromium@1.5.208: {} emoji-regex@8.0.0: {} @@ -4566,7 +4565,7 @@ snapshots: domhandler: 5.0.3 htmlparser2: 10.0.0 - html-react-parser@5.2.6(@types/react@19.1.10)(react@19.1.1): + html-react-parser@5.2.6(@types/react@19.1.11)(react@19.1.1): dependencies: domhandler: 5.0.3 html-dom-parser: 5.1.1 @@ -4574,7 +4573,7 @@ snapshots: react-property: 2.0.2 style-to-js: 1.1.17 optionalDependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.11 html-url-attributes@3.0.1: {} @@ -4840,9 +4839,9 @@ snapshots: type-fest: 0.18.1 yargs-parser: 20.2.9 - merge-refs@2.0.0(@types/react@19.1.10): + merge-refs@2.0.0(@types/react@19.1.11): optionalDependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.11 micromark-core-commonmark@2.0.3: dependencies: @@ -5191,7 +5190,7 @@ snapshots: react-datepicker@8.7.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@floating-ui/react': 0.27.15(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@floating-ui/react': 0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1) clsx: 2.1.1 date-fns: 4.1.0 react: 19.1.1 @@ -5225,11 +5224,11 @@ snapshots: dependencies: react: 19.1.1 - react-markdown@10.1.0(@types/react@19.1.10)(react@19.1.1): + react-markdown@10.1.0(@types/react@19.1.11)(react@19.1.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.1.10 + '@types/react': 19.1.11 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -5251,13 +5250,13 @@ snapshots: qr.js: 0.0.0 react: 19.1.1 - react-redux@9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1): + react-redux@9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 react: 19.1.1 use-sync-external-store: 1.5.0(react@19.1.1) optionalDependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.11 redux: 5.0.1 react-resize-detector@12.1.0(react@19.1.1): @@ -5346,9 +5345,9 @@ snapshots: dependencies: picomatch: 2.3.1 - recharts@3.1.2(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react-is@19.1.1)(react@19.1.1)(redux@5.0.1): + recharts@3.1.2(@types/react@19.1.11)(react-dom@19.1.1(react@19.1.1))(react-is@19.1.1)(react@19.1.1)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1))(react@19.1.1) + '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1))(react@19.1.1) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.39.10 @@ -5357,7 +5356,7 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) react-is: 19.1.1 - react-redux: 9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1) + react-redux: 9.2.0(@types/react@19.1.11)(react@19.1.1)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3 use-sync-external-store: 1.5.0(react@19.1.1) @@ -5428,30 +5427,30 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.46.4: + rollup@4.48.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.4 - '@rollup/rollup-android-arm64': 4.46.4 - '@rollup/rollup-darwin-arm64': 4.46.4 - '@rollup/rollup-darwin-x64': 4.46.4 - '@rollup/rollup-freebsd-arm64': 4.46.4 - '@rollup/rollup-freebsd-x64': 4.46.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.4 - '@rollup/rollup-linux-arm-musleabihf': 4.46.4 - '@rollup/rollup-linux-arm64-gnu': 4.46.4 - '@rollup/rollup-linux-arm64-musl': 4.46.4 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.4 - '@rollup/rollup-linux-ppc64-gnu': 4.46.4 - '@rollup/rollup-linux-riscv64-gnu': 4.46.4 - '@rollup/rollup-linux-riscv64-musl': 4.46.4 - '@rollup/rollup-linux-s390x-gnu': 4.46.4 - '@rollup/rollup-linux-x64-gnu': 4.46.4 - '@rollup/rollup-linux-x64-musl': 4.46.4 - '@rollup/rollup-win32-arm64-msvc': 4.46.4 - '@rollup/rollup-win32-ia32-msvc': 4.46.4 - '@rollup/rollup-win32-x64-msvc': 4.46.4 + '@rollup/rollup-android-arm-eabi': 4.48.1 + '@rollup/rollup-android-arm64': 4.48.1 + '@rollup/rollup-darwin-arm64': 4.48.1 + '@rollup/rollup-darwin-x64': 4.48.1 + '@rollup/rollup-freebsd-arm64': 4.48.1 + '@rollup/rollup-freebsd-x64': 4.48.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.48.1 + '@rollup/rollup-linux-arm-musleabihf': 4.48.1 + '@rollup/rollup-linux-arm64-gnu': 4.48.1 + '@rollup/rollup-linux-arm64-musl': 4.48.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.48.1 + '@rollup/rollup-linux-ppc64-gnu': 4.48.1 + '@rollup/rollup-linux-riscv64-gnu': 4.48.1 + '@rollup/rollup-linux-riscv64-musl': 4.48.1 + '@rollup/rollup-linux-s390x-gnu': 4.48.1 + '@rollup/rollup-linux-x64-gnu': 4.48.1 + '@rollup/rollup-linux-x64-musl': 4.48.1 + '@rollup/rollup-win32-arm64-msvc': 4.48.1 + '@rollup/rollup-win32-ia32-msvc': 4.48.1 + '@rollup/rollup-win32-x64-msvc': 4.48.1 fsevents: 2.3.3 rxjs@7.8.2: @@ -5879,7 +5878,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.46.4 + rollup: 4.48.1 tinyglobby: 0.2.14 optionalDependencies: '@types/node': 24.3.0 @@ -5969,9 +5968,9 @@ snapshots: zod@3.25.76: {} - zustand@5.0.8(@types/react@19.1.10)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + zustand@5.0.8(@types/react@19.1.11)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): optionalDependencies: - '@types/react': 19.1.10 + '@types/react': 19.1.11 immer: 10.1.1 react: 19.1.1 use-sync-external-store: 1.5.0(react@19.1.1)