diff --git a/Cargo.lock b/Cargo.lock index 593435f29..35da6c241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,6 +1131,7 @@ dependencies = [ "rust-ini", "secp256k1", "secrecy", + "semver", "serde", "serde_cbor_2", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 3e5bb99c2..09bebc254 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ secp256k1 = { version = "0.29", features = [ "global-context", ] } secrecy = { version = "0.8", features = ["serde"] } +semver = "1.0" serde = { version = "1.0", features = ["derive"] } # match version from webauthn-rs-core serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 945cb07c8..03d65efb1 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -30,6 +30,7 @@ pub mod openid_flow; pub(crate) mod settings; pub(crate) mod ssh_authorized_keys; pub(crate) mod support; +pub(crate) mod updates; pub(crate) mod user; pub(crate) mod webhooks; #[cfg(feature = "wireguard")] diff --git a/src/handlers/updates.rs b/src/handlers/updates.rs new file mode 100644 index 000000000..10e9584ae --- /dev/null +++ b/src/handlers/updates.rs @@ -0,0 +1,29 @@ +use axum::http::StatusCode; +use serde_json::json; + +use super::{ApiResponse, ApiResult}; +use crate::{ + auth::{AdminRole, SessionInfo}, + updates::get_update, +}; + +pub async fn check_new_version(_admin: AdminRole, session: SessionInfo) -> ApiResult { + debug!( + "User {} is checking if there is a new version available", + session.user.username + ); + let update = get_update(); + if let Some(update) = update.as_ref() { + debug!("A new version is available, returning the update information"); + Ok(ApiResponse { + json: json!(update), + status: StatusCode::OK, + }) + } else { + debug!("No new version available"); + Ok(ApiResponse { + json: serde_json::json!({ "message": "No updates available" }), + status: StatusCode::NO_CONTENT, + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index ca6e016c1..50004c42d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ use handlers::{ add_authentication_key, delete_authentication_key, fetch_authentication_keys, rename_authentication_key, }, + updates::check_new_version, yubikey::{delete_yubikey, rename_yubikey}, }; use ipnetwork::IpNetwork; @@ -130,6 +131,7 @@ pub(crate) mod random; pub mod secret; pub mod support; pub mod templates; +pub mod updates; pub mod utility_thread; pub mod wg_config; pub mod wireguard_peer_disconnect; @@ -299,6 +301,7 @@ pub fn build_webapp( .route("/info", get(get_app_info)) .route("/ssh_authorized_keys", get(get_authorized_keys)) .route("/api-docs", get(openapi)) + .route("/updates", get(check_new_version)) // /auth .route("/auth", post(authenticate)) .route("/auth/logout", post(logout)) diff --git a/src/updates.rs b/src/updates.rs new file mode 100644 index 000000000..911739ef8 --- /dev/null +++ b/src/updates.rs @@ -0,0 +1,71 @@ +use std::{ + env, + sync::{RwLock, RwLockReadGuard}, +}; + +use chrono::NaiveDate; +use semver::Version; + +const PRODUCT_NAME: &str = "Defguard"; +const UPDATES_URL: &str = "https://update-service-dev.defguard.net/api/update/check"; +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Deserialize, Debug, Serialize)] +pub struct Update { + version: String, + release_date: NaiveDate, + release_notes_url: String, + update_url: String, + critical: bool, + notes: String, +} + +static NEW_UPDATE: RwLock> = RwLock::new(None); + +fn set_update(update: Update) { + *NEW_UPDATE + .write() + .expect("Failed to acquire lock on the update.") = Some(update); +} + +pub fn get_update() -> RwLockReadGuard<'static, Option> { + NEW_UPDATE + .read() + .expect("Failed to acquire lock on the update.") +} + +async fn fetch_update() -> Result { + let body = serde_json::json!({ + "product": PRODUCT_NAME, + "client_version": VERSION, + "operating_system": env::consts::OS, + }); + let response = reqwest::Client::new() + .post(UPDATES_URL) + .json(&body) + .send() + .await?; + Ok(response.json::().await?) +} + +pub(crate) async fn do_new_version_check() -> Result<(), anyhow::Error> { + debug!("Checking for new version of Defguard ..."); + let update = fetch_update().await?; + let current_version = Version::parse(VERSION)?; + let new_version = Version::parse(&update.version)?; + if new_version > current_version { + if update.critical { + warn!("There is a new critical Defguard update available: {} (Released on {}). It's recommended to update as soon as possible.", + update.version, update.release_date); + } else { + info!( + "There is a new Defguard version available: {} (Released on {})", + update.version, update.release_date + ); + } + set_update(update); + } else { + debug!("New version check done. You are using the latest version of Defguard."); + } + Ok(()) +} diff --git a/src/utility_thread.rs b/src/utility_thread.rs index d79aa230b..c136c376f 100644 --- a/src/utility_thread.rs +++ b/src/utility_thread.rs @@ -3,17 +3,22 @@ use std::time::Duration; use sqlx::PgPool; use tokio::time::{sleep, Instant}; -use crate::enterprise::{ - directory_sync::{do_directory_sync, get_directory_sync_interval}, - limits::do_count_update, +use crate::{ + enterprise::{ + directory_sync::{do_directory_sync, get_directory_sync_interval}, + limits::do_count_update, + }, + updates::do_new_version_check, }; const UTILITY_THREAD_MAIN_SLEEP_TIME: u64 = 5; const COUNT_UPDATE_INTERVAL: u64 = 60 * 60; +const UPDATES_CHECK_INTERVAL: u64 = 60 * 60 * 6; pub async fn run_utility_thread(pool: &PgPool) -> Result<(), anyhow::Error> { let mut last_count_update = Instant::now(); let mut last_directory_sync = Instant::now(); + let mut last_updates_check = Instant::now(); let directory_sync_task = || async { if let Err(e) = do_directory_sync(pool).await { @@ -27,8 +32,15 @@ pub async fn run_utility_thread(pool: &PgPool) -> Result<(), anyhow::Error> { } }; + let updates_check_task = || async { + if let Err(e) = do_new_version_check().await { + error!("There was an error while checking for new Defguard version: {e:?}"); + } + }; + directory_sync_task().await; count_update_task().await; + updates_check_task().await; loop { sleep(Duration::from_secs(UTILITY_THREAD_MAIN_SLEEP_TIME)).await; @@ -44,5 +56,11 @@ pub async fn run_utility_thread(pool: &PgPool) -> Result<(), anyhow::Error> { directory_sync_task().await; last_directory_sync = Instant::now(); } + + // Check for new Defguard version + if last_updates_check.elapsed().as_secs() >= UPDATES_CHECK_INTERVAL { + updates_check_task().await; + last_updates_check = Instant::now(); + } } } diff --git a/web/.env b/web/.env new file mode 100644 index 000000000..3afe802fb --- /dev/null +++ b/web/.env @@ -0,0 +1 @@ +PROXY_TARGET=https://defguard-dev.teonite.net diff --git a/web/package.json b/web/package.json index 544013ef8..8b284ff41 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,7 @@ "serve": "vite preview", "generate-translation-types": "typesafe-i18n --no-watch", "lint": "eslint -c ./.eslintrc.cjs --ignore-path .eslintignore --ext .ts --ext .tsx src/ && prettier --check 'src/**/*.{ts,tsx,scss}' && tsc", - "fix": "prettier -w 'src/**/*.{ts,tsx,scss}' && eslint -c ./.eslintrc.cjs --ignore-path .eslintignore --fix 'src/**/*.{ts,tsx}'", + "fix": "prettier -w 'src/**/*.{ts,tsx,scss}' && eslint -c ./.eslintrc.cjs --ignore-path .eslintignore --fix --ext .ts --ext .tsx src/", "parse-core-svgs": "svgr --no-index --jsx-runtime 'automatic' --svgo-config ./svgo.config.json --prettier-config ./.prettierrc --out-dir ./src/shared/components/svg/ --typescript ./src/shared/images/svg/", "parse-ui-svgs": "svgr --no-index --jsx-runtime 'automatic' --svgo-config ./svgo.config.json --prettier-config ./.prettierrc --out-dir ./src/shared/defguard-ui/components/svg/ --typescript ./src/shared/defguard-ui/images/svg/", "parse-svgs": "pnpm parse-ui-svgs && pnpm parse-core-svgs", @@ -49,8 +49,8 @@ "@react-rxjs/core": "^0.10.7", "@stablelib/base64": "^1.0.1", "@stablelib/x25519": "^1.0.3", - "@tanstack/query-core": "^4.32.6", - "@tanstack/react-query": "^4.32.6", + "@tanstack/query-core": "^4.36.1", + "@tanstack/react-query": "^4.36.1", "@tanstack/react-virtual": "3.0.0-beta.9", "@tanstack/virtual-core": "3.0.0-beta.9", "@tauri-apps/api": "^1.5.3", @@ -91,7 +91,6 @@ "react-virtualized-auto-sizer": "^1.0.21", "react-window": "^1.8.10", "recharts": "^2.10.4", - "rollup": "^4.9.6", "rxjs": "^7.8.1", "terser": "^5.27.0", "typesafe-i18n": "^5.26.2", @@ -104,8 +103,9 @@ "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3", "@hookform/devtools": "^4.3.1", + "@rollup/wasm-node": "^4.28.1", "@svgr/cli": "^8.1.0", - "@tanstack/react-query-devtools": "^4.32.6", + "@tanstack/react-query-devtools": "^4.36.1", "@types/byte-size": "^8.1.2", "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", @@ -118,6 +118,7 @@ "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.17", "concurrently": "^8.2.2", + "dotenv": "^16.4.7", "esbuild": "^0.19.12", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", @@ -131,6 +132,7 @@ "postcss": "^8.4.33", "prettier": "^3.2.4", "prop-types": "^15.8.1", + "rollup": "npm:@rollup/wasm-node@^4.28.1", "rollup-plugin-preserve-directives": "^0.3.1", "sass": "^1.70.0", "standard-version": "^9.5.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 468706420..ed8b52def 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -36,10 +36,10 @@ importers: specifier: ^1.0.3 version: 1.0.3 '@tanstack/query-core': - specifier: ^4.32.6 + specifier: ^4.36.1 version: 4.36.1 '@tanstack/react-query': - specifier: ^4.32.6 + specifier: ^4.36.1 version: 4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tanstack/react-virtual': specifier: 3.0.0-beta.9 @@ -161,9 +161,6 @@ importers: recharts: specifier: ^2.10.4 version: 2.10.4(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - rollup: - specifier: npm:@rollup/wasm-node - version: '@rollup/wasm-node@4.24.0' rxjs: specifier: ^7.8.1 version: 7.8.1 @@ -195,11 +192,14 @@ importers: '@hookform/devtools': specifier: ^4.3.1 version: 4.3.1(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rollup/wasm-node': + specifier: ^4.28.1 + version: 4.28.1 '@svgr/cli': specifier: ^8.1.0 version: 8.1.0(typescript@5.3.3) '@tanstack/react-query-devtools': - specifier: ^4.32.6 + specifier: ^4.36.1 version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/byte-size': specifier: ^8.1.2 @@ -237,6 +237,9 @@ importers: concurrently: specifier: ^8.2.2 version: 8.2.2 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 esbuild: specifier: ^0.19.12 version: 0.19.12 @@ -276,9 +279,12 @@ importers: prop-types: specifier: ^15.8.1 version: 15.8.1 + rollup: + specifier: npm:@rollup/wasm-node + version: '@rollup/wasm-node@4.28.1' rollup-plugin-preserve-directives: specifier: ^0.3.1 - version: 0.3.1(@rollup/wasm-node@4.24.0) + version: 0.3.1(@rollup/wasm-node@4.28.1) sass: specifier: ^1.70.0 version: 1.70.0 @@ -942,19 +948,29 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^2.2.3 + '@csstools/css-parser-algorithms@2.7.1': + resolution: {integrity: sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-tokenizer': ^2.4.1 + '@csstools/css-tokenizer@2.2.3': resolution: {integrity: sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==} engines: {node: ^14 || ^16 || >=18} - '@csstools/media-query-list-parser@2.1.7': - resolution: {integrity: sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==} + '@csstools/css-tokenizer@2.4.1': + resolution: {integrity: sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==} + engines: {node: ^14 || ^16 || >=18} + + '@csstools/media-query-list-parser@2.1.13': + resolution: {integrity: sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - '@csstools/css-parser-algorithms': ^2.5.0 - '@csstools/css-tokenizer': ^2.2.3 + '@csstools/css-parser-algorithms': ^2.7.1 + '@csstools/css-tokenizer': ^2.4.1 - '@csstools/selector-specificity@3.0.1': - resolution: {integrity: sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==} + '@csstools/selector-specificity@3.1.1': + resolution: {integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss-selector-parser: ^6.0.13 @@ -1228,10 +1244,6 @@ packages: resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} engines: {node: '>=6.9.0'} - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - '@jridgewell/gen-mapping@0.3.3': resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -1316,10 +1328,6 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1341,8 +1349,8 @@ packages: resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} - '@rollup/wasm-node@4.24.0': - resolution: {integrity: sha512-LL6oALR6fKG6GihtH0K0uWLAl19Q/QJst+oKJT1VWwFo4sPLA0/7JeZaSqrpFWq8OPloiKx/NDG4BWppFSX2vQ==} + '@rollup/wasm-node@4.28.1': + resolution: {integrity: sha512-t4ckEC09V3wbe0r6T4fGjq85lEbvGcGxn7QYYgjHyKNzZaQU5kFqr4FsavXYHRiVNYq8m+dRhdGjpfcC9UzzPg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1836,8 +1844,8 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -2073,8 +2081,8 @@ packages: resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} engines: {node: '>=14.16'} - caniuse-lite@1.0.30001580: - resolution: {integrity: sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==} + caniuse-lite@1.0.30001687: + resolution: {integrity: sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2338,8 +2346,8 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} - css-functions-list@3.2.1: - resolution: {integrity: sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==} + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} engines: {node: '>=12 || >=16'} css-select@5.1.0: @@ -2454,6 +2462,15 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -2584,6 +2601,10 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dotgitignore@2.1.0: resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} engines: {node: '>=6'} @@ -2852,6 +2873,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} @@ -2905,13 +2929,16 @@ packages: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} - flat-cache@4.0.0: - resolution: {integrity: sha512-EryKbCE/wxpxKniQlyas6PY1I9vwtF3uCBweX+N8KYTCn3Y12RTGtQAJ/bd5pl7kxUAc8v/R3Ake/N17OZiFqA==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} flatted@3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + follow-redirects@1.15.5: resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} @@ -2924,10 +2951,6 @@ packages: for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} - form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -3024,11 +3047,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -3228,6 +3246,10 @@ packages: resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} engines: {node: '>= 4'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} @@ -3472,10 +3494,6 @@ packages: itertools@2.2.3: resolution: {integrity: sha512-TV4TDJ2FrLxhRJDX/AgdyI76i6cHi2Z1hml/d+HLcGVHxmgfxsLpoQBN2ZE9OizPt10+VW+LamLfCDASlnxvNg==} - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3632,10 +3650,6 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lru-cache@10.1.0: - resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} - engines: {node: 14 || >=16.14} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3734,8 +3748,8 @@ packages: memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - meow@13.1.0: - resolution: {integrity: sha512-o5R/R3Tzxq0PJ3v3qcQJtSvSE9nKOLSAaDuuoMzDVuGTwHdccMWcYomh9Xolng2tjT6O/Y83d+0coVGof6tqmA==} + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} meow@8.1.2: @@ -3858,6 +3872,10 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -3896,10 +3914,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} - engines: {node: '>=16 || 14 >=14.17'} - modify-values@1.0.1: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} engines: {node: '>=0.10.0'} @@ -3928,6 +3942,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -4127,10 +4146,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} - path-to-regexp@6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} @@ -4152,6 +4167,9 @@ packages: picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -4174,8 +4192,11 @@ packages: postcss-resolve-nested-selector@0.1.1: resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} - postcss-safe-parser@7.0.0: - resolution: {integrity: sha512-ovehqRNVCpuFzbXoTb4qLtyzK3xn3t/CUBxOs8LsnQjQrShaB4lKiHoVqY8ANaC0hBMHq5QVWk77rwGklFUDrg==} + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} engines: {node: '>=18.0'} peerDependencies: postcss: ^8.4.31 @@ -4190,6 +4211,10 @@ packages: resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==} engines: {node: '>=4'} + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -4197,6 +4222,10 @@ packages: resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4520,11 +4549,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@5.0.5: - resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} - engines: {node: '>=14'} - hasBin: true - rollup-plugin-preserve-directives@0.3.1: resolution: {integrity: sha512-Jn1gWU7G55A1sU6eFpXmwknfBasF0XbBzRqsE6nqrb/gun+mGV7nx++CwOSGPJQpFzFqvKm5U4XNKo3LTLi4Hg==} peerDependencies: @@ -4639,6 +4663,10 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -4838,8 +4866,8 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - supports-hyperlinks@3.0.0: - resolution: {integrity: sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==} + supports-hyperlinks@3.1.0: + resolution: {integrity: sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==} engines: {node: '>=14.18'} supports-preserve-symlinks-flag@1.0.0: @@ -4867,8 +4895,8 @@ packages: tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - table@6.8.1: - resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} terser@5.27.0: @@ -6134,16 +6162,22 @@ snapshots: dependencies: '@csstools/css-tokenizer': 2.2.3 + '@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-tokenizer': 2.4.1 + '@csstools/css-tokenizer@2.2.3': {} - '@csstools/media-query-list-parser@2.1.7(@csstools/css-parser-algorithms@2.5.0(@csstools/css-tokenizer@2.2.3))(@csstools/css-tokenizer@2.2.3)': + '@csstools/css-tokenizer@2.4.1': {} + + '@csstools/media-query-list-parser@2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': dependencies: - '@csstools/css-parser-algorithms': 2.5.0(@csstools/css-tokenizer@2.2.3) - '@csstools/css-tokenizer': 2.2.3 + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 - '@csstools/selector-specificity@3.0.1(postcss-selector-parser@6.0.15)': + '@csstools/selector-specificity@3.1.1(postcss-selector-parser@6.1.2)': dependencies: - postcss-selector-parser: 6.0.15 + postcss-selector-parser: 6.1.2 '@emotion/babel-plugin@11.11.0': dependencies: @@ -6382,15 +6416,6 @@ snapshots: '@hutson/parse-repository-url@3.0.2': {} - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - '@jridgewell/gen-mapping@0.3.3': dependencies: '@jridgewell/set-array': 1.1.2 @@ -6548,9 +6573,6 @@ snapshots: '@open-draft/until@2.1.0': {} - '@pkgjs/parseargs@0.11.0': - optional: true - '@pkgr/core@0.1.1': {} '@reach/observe-rect@1.2.0': {} @@ -6569,7 +6591,7 @@ snapshots: estree-walker: 2.0.2 picomatch: 2.3.1 - '@rollup/wasm-node@4.24.0': + '@rollup/wasm-node@4.28.1': dependencies: '@types/estree': 1.0.6 optionalDependencies: @@ -6797,7 +6819,7 @@ snapshots: '@types/acorn@4.0.6': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 '@types/babel__core@7.20.5': dependencies: @@ -7095,12 +7117,12 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.12.0: + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - uri-js: 4.4.1 ansi-align@3.0.1: dependencies: @@ -7211,7 +7233,7 @@ snapshots: autoprefixer@10.4.17(postcss@8.4.33): dependencies: browserslist: 4.22.2 - caniuse-lite: 1.0.30001580 + caniuse-lite: 1.0.30001687 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -7320,7 +7342,7 @@ snapshots: browserslist@4.22.2: dependencies: - caniuse-lite: 1.0.30001580 + caniuse-lite: 1.0.30001687 electron-to-chromium: 1.4.645 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) @@ -7363,7 +7385,7 @@ snapshots: camelcase@7.0.1: {} - caniuse-lite@1.0.30001580: {} + caniuse-lite@1.0.30001687: {} ccount@2.0.1: {} @@ -7667,7 +7689,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-functions-list@3.2.1: {} + css-functions-list@3.2.3: {} css-select@5.1.0: dependencies: @@ -7759,6 +7781,10 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.4.0: + dependencies: + ms: 2.1.3 + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 @@ -7879,6 +7905,8 @@ snapshots: dependencies: is-obj: 2.0.0 + dotenv@16.4.7: {} + dotgitignore@2.1.0: dependencies: find-up: 3.0.0 @@ -8302,6 +8330,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.0.3: {} + fastest-levenshtein@1.0.16: {} fastq@1.16.0: @@ -8318,7 +8348,7 @@ snapshots: file-entry-cache@8.0.0: dependencies: - flat-cache: 4.0.0 + flat-cache: 4.0.1 file-saver@2.0.5: {} @@ -8354,25 +8384,21 @@ snapshots: keyv: 4.5.4 rimraf: 3.0.2 - flat-cache@4.0.0: + flat-cache@4.0.1: dependencies: - flatted: 3.2.9 + flatted: 3.3.2 keyv: 4.5.4 - rimraf: 5.0.5 flatted@3.2.9: {} + flatted@3.3.2: {} + follow-redirects@1.15.5: {} for-each@0.3.3: dependencies: is-callable: 1.2.7 - foreground-child@3.1.1: - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - form-data@4.0.0: dependencies: asynckit: 0.4.0 @@ -8466,14 +8492,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.3.10: - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 - path-scurry: 1.10.1 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -8771,6 +8789,8 @@ snapshots: ignore@5.3.0: {} + ignore@5.3.2: {} + immutable@4.3.4: {} import-fresh@3.3.0: @@ -8989,12 +9009,6 @@ snapshots: itertools@2.2.3: {} - jackspeak@2.3.6: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -9154,8 +9168,6 @@ snapshots: dependencies: tslib: 2.6.2 - lru-cache@10.1.0: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -9353,7 +9365,7 @@ snapshots: memoize-one@5.2.1: {} - meow@13.1.0: {} + meow@13.2.0: {} meow@8.1.2: dependencies: @@ -9452,7 +9464,7 @@ snapshots: micromark-extension-mdx-expression@3.0.0: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 devlop: 1.1.0 micromark-factory-mdx-expression: 2.0.1 micromark-factory-space: 2.0.0 @@ -9464,7 +9476,7 @@ snapshots: micromark-extension-mdx-jsx@3.0.0: dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 micromark-factory-mdx-expression: 2.0.1 @@ -9480,7 +9492,7 @@ snapshots: micromark-extension-mdxjs-esm@3.0.0: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 micromark-util-character: 2.0.1 @@ -9516,7 +9528,7 @@ snapshots: micromark-factory-mdx-expression@2.0.1: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 devlop: 1.1.0 micromark-util-character: 2.0.1 micromark-util-events-to-acorn: 2.0.2 @@ -9580,7 +9592,7 @@ snapshots: micromark-util-events-to-acorn@2.0.2: dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 '@types/unist': 3.0.2 devlop: 1.1.0 estree-util-visit: 2.0.0 @@ -9642,6 +9654,11 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mime-db@1.52.0: {} mime-types@2.1.35: @@ -9674,8 +9691,6 @@ snapshots: minimist@1.2.8: {} - minipass@7.0.4: {} - modify-values@1.0.1: {} ms@2.1.2: {} @@ -9709,6 +9724,8 @@ snapshots: nanoid@3.3.7: {} + nanoid@3.3.8: {} + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -9928,11 +9945,6 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.10.1: - dependencies: - lru-cache: 10.1.0 - minipass: 7.0.4 - path-to-regexp@6.2.1: {} path-type@3.0.0: @@ -9951,6 +9963,8 @@ snapshots: picocolors@1.0.0: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} pify@2.3.0: {} @@ -9963,9 +9977,11 @@ snapshots: postcss-resolve-nested-selector@0.1.1: {} - postcss-safe-parser@7.0.0(postcss@8.4.33): + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@7.0.1(postcss@8.4.49): dependencies: - postcss: 8.4.33 + postcss: 8.4.49 postcss-scss@4.0.9(postcss@8.4.33): dependencies: @@ -9976,6 +9992,11 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-value-parser@4.2.0: {} postcss@8.4.33: @@ -9984,6 +10005,12 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.0.2 + postcss@8.4.49: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -10348,14 +10375,10 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@5.0.5: - dependencies: - glob: 10.3.10 - - rollup-plugin-preserve-directives@0.3.1(@rollup/wasm-node@4.24.0): + rollup-plugin-preserve-directives@0.3.1(@rollup/wasm-node@4.28.1): dependencies: magic-string: 0.30.5 - rollup: '@rollup/wasm-node@4.24.0' + rollup: '@rollup/wasm-node@4.28.1' run-applescript@5.0.0: dependencies: @@ -10468,6 +10491,8 @@ snapshots: source-map-js@1.0.2: {} + source-map-js@1.2.1: {} + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -10660,16 +10685,16 @@ snapshots: stylelint@16.2.0(typescript@5.3.3): dependencies: - '@csstools/css-parser-algorithms': 2.5.0(@csstools/css-tokenizer@2.2.3) - '@csstools/css-tokenizer': 2.2.3 - '@csstools/media-query-list-parser': 2.1.7(@csstools/css-parser-algorithms@2.5.0(@csstools/css-tokenizer@2.2.3))(@csstools/css-tokenizer@2.2.3) - '@csstools/selector-specificity': 3.0.1(postcss-selector-parser@6.0.15) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.2) balanced-match: 2.0.0 colord: 2.9.3 cosmiconfig: 9.0.0(typescript@5.3.3) - css-functions-list: 3.2.1 + css-functions-list: 3.2.3 css-tree: 2.3.1 - debug: 4.3.4 + debug: 4.4.0 fast-glob: 3.3.2 fastest-levenshtein: 1.0.16 file-entry-cache: 8.0.0 @@ -10677,26 +10702,26 @@ snapshots: globby: 11.1.0 globjoin: 0.1.4 html-tags: 3.3.1 - ignore: 5.3.0 + ignore: 5.3.2 imurmurhash: 0.1.4 is-plain-object: 5.0.0 known-css-properties: 0.29.0 mathml-tag-names: 2.1.3 - meow: 13.1.0 - micromatch: 4.0.5 + meow: 13.2.0 + micromatch: 4.0.8 normalize-path: 3.0.0 - picocolors: 1.0.0 - postcss: 8.4.33 - postcss-resolve-nested-selector: 0.1.1 - postcss-safe-parser: 7.0.0(postcss@8.4.33) - postcss-selector-parser: 6.0.15 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.4.49) + postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 resolve-from: 5.0.0 string-width: 4.2.3 strip-ansi: 7.1.0 - supports-hyperlinks: 3.0.0 + supports-hyperlinks: 3.1.0 svg-tags: 1.0.0 - table: 6.8.1 + table: 6.9.0 write-file-atomic: 5.0.1 transitivePeerDependencies: - supports-color @@ -10720,7 +10745,7 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-hyperlinks@3.0.0: + supports-hyperlinks@3.1.0: dependencies: has-flag: 4.0.0 supports-color: 7.2.0 @@ -10750,9 +10775,9 @@ snapshots: tabbable@6.2.0: {} - table@6.8.1: + table@6.9.0: dependencies: - ajv: 8.12.0 + ajv: 8.17.1 lodash.truncate: 4.4.2 slice-ansi: 4.0.0 string-width: 4.2.3 @@ -11066,7 +11091,7 @@ snapshots: '@rollup/pluginutils': 4.2.1 '@types/eslint': 8.56.2 eslint: 8.56.0 - rollup: '@rollup/wasm-node@4.24.0' + rollup: '@rollup/wasm-node@4.28.1' vite: 5.0.12(@types/node@20.11.7)(sass@1.70.0)(terser@5.27.0) vite-plugin-package-version@1.1.0(vite@5.0.12(@types/node@20.11.7)(sass@1.70.0)(terser@5.27.0)): @@ -11088,7 +11113,7 @@ snapshots: dependencies: esbuild: 0.19.12 postcss: 8.4.33 - rollup: '@rollup/wasm-node@4.24.0' + rollup: '@rollup/wasm-node@4.28.1' optionalDependencies: '@types/node': 20.11.7 fsevents: 2.3.3 diff --git a/web/src/components/App/App.tsx b/web/src/components/App/App.tsx index 6e9414341..b5bcb1f9d 100644 --- a/web/src/components/App/App.tsx +++ b/web/src/components/App/App.tsx @@ -20,6 +20,7 @@ import { UsersSharedModals } from '../../pages/users/UsersSharedModals'; import { WebhooksListPage } from '../../pages/webhooks/WebhooksListPage'; import { WizardPage } from '../../pages/wizard/WizardPage'; import { PageContainer } from '../../shared/components/Layout/PageContainer/PageContainer'; +import { UpdateNotificationModal } from '../../shared/components/modals/UpdateNotificationModal/UpdateNotificationModal'; import { ProtectedRoute } from '../../shared/components/Router/Guards/ProtectedRoute/ProtectedRoute'; import { ToastManager } from '../../shared/defguard-ui/components/Layout/ToastManager/ToastManager'; import { useAuthStore } from '../../shared/hooks/store/useAuthStore'; @@ -180,6 +181,7 @@ const App = () => { /> + diff --git a/web/src/components/AppLoader.tsx b/web/src/components/AppLoader.tsx index eabf80489..527703ae3 100644 --- a/web/src/components/AppLoader.tsx +++ b/web/src/components/AppLoader.tsx @@ -12,6 +12,7 @@ import { LoaderPage } from '../pages/loader/LoaderPage'; import { isUserAdmin } from '../shared/helpers/isUserAdmin'; import { useAppStore } from '../shared/hooks/store/useAppStore'; import { useAuthStore } from '../shared/hooks/store/useAuthStore'; +import { useUpdatesStore } from '../shared/hooks/store/useUpdatesStore'; import useApi from '../shared/hooks/useApi'; import { useToaster } from '../shared/hooks/useToaster'; import { QueryKeys } from '../shared/queries'; @@ -28,6 +29,7 @@ export const AppLoader = () => { const appSettings = useAppStore((state) => state.settings); const { getAppInfo, + getNewVersion, user: { getMe }, getEnterpriseStatus, settings: { getEssentialSettings, getEnterpriseSettings }, @@ -37,6 +39,8 @@ export const AppLoader = () => { const activeLanguage = useAppStore((state) => state.language); const setAppStore = useAppStore((state) => state.setState); const { LL } = useI18nContext(); + const setUpdateStore = useUpdatesStore((s) => s.setUpdate); + const clearUpdate = useUpdatesStore((s) => s.clearUpdate); useQuery([QueryKeys.FETCH_ME], getMe, { onSuccess: async (user) => { @@ -134,6 +138,22 @@ export const AppLoader = () => { } }, [essentialSettings, setAppStore]); + useQuery([QueryKeys.FETCH_NEW_VERSION], getNewVersion, { + onSuccess: (data) => { + if (!data) { + clearUpdate(); + } else { + setUpdateStore(data); + } + }, + onError: (err) => { + console.error(err); + }, + refetchOnWindowFocus: false, + retry: false, + enabled: !isUndefined(currentUser) && isUserAdmin(currentUser), + }); + if (userLoading || (settingsLoading && isUndefined(appSettings))) { return ; } diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 6669c79fc..1db374037 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -24,6 +24,7 @@ const en: BaseTranslation = { rename: 'Rename', copy: 'Copy', edit: 'Edit', + dismiss: 'Dismiss', }, key: 'Key', name: 'Name', @@ -40,6 +41,22 @@ const en: BaseTranslation = { }, }, modals: { + updatesNotificationToaster: { + title: 'New version available {version: string}', + controls: { + more: "See what's new", + }, + }, + updatesNotification: { + header: { + title: 'Update Available', + newVersion: 'new version {version: string}', + criticalBadge: 'critical update', + }, + controls: { + visitRelease: 'Visit release page', + }, + }, addGroup: { title: 'Add group', selectAll: 'Select all users', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index cb25e3fe1..d721a88a5 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -91,6 +91,10 @@ type RootTranslation = { * E​d​i​t */ edit: string + /** + * D​i​s​m​i​s​s + */ + dismiss: string } /** * K​e​y @@ -134,6 +138,42 @@ type RootTranslation = { } } modals: { + updatesNotificationToaster: { + /** + * N​e​w​ ​v​e​r​s​i​o​n​ ​a​v​a​i​l​a​b​l​e​ ​{​v​e​r​s​i​o​n​} + * @param {string} version + */ + title: RequiredParams<'version'> + controls: { + /** + * S​e​e​ ​w​h​a​t​'​s​ ​n​e​w + */ + more: string + } + } + updatesNotification: { + header: { + /** + * U​p​d​a​t​e​ ​A​v​a​i​l​a​b​l​e + */ + title: string + /** + * n​e​w​ ​v​e​r​s​i​o​n​ ​{​v​e​r​s​i​o​n​} + * @param {string} version + */ + newVersion: RequiredParams<'version'> + /** + * c​r​i​t​i​c​a​l​ ​u​p​d​a​t​e + */ + criticalBadge: string + } + controls: { + /** + * V​i​s​i​t​ ​r​e​l​e​a​s​e​ ​p​a​g​e + */ + visitRelease: string + } + } addGroup: { /** * A​d​d​ ​g​r​o​u​p @@ -4474,6 +4514,10 @@ export type TranslationFunctions = { * Edit */ edit: () => LocalizedString + /** + * Dismiss + */ + dismiss: () => LocalizedString } /** * Key @@ -4517,6 +4561,40 @@ export type TranslationFunctions = { } } modals: { + updatesNotificationToaster: { + /** + * New version available {version} + */ + title: (arg: { version: string }) => LocalizedString + controls: { + /** + * See what's new + */ + more: () => LocalizedString + } + } + updatesNotification: { + header: { + /** + * Update Available + */ + title: () => LocalizedString + /** + * new version {version} + */ + newVersion: (arg: { version: string }) => LocalizedString + /** + * critical update + */ + criticalBadge: () => LocalizedString + } + controls: { + /** + * Visit release page + */ + visitRelease: () => LocalizedString + } + } addGroup: { /** * Add group diff --git a/web/src/i18n/ko/index.ts b/web/src/i18n/ko/index.ts index 802a99cd3..6c41aacf9 100644 --- a/web/src/i18n/ko/index.ts +++ b/web/src/i18n/ko/index.ts @@ -1,7 +1,8 @@ /* eslint-disable max-len */ -import type { BaseTranslation } from '../i18n-types'; +import en from '../en'; +import { extendDictionary } from '../i18n-util'; -const ko: BaseTranslation = { +const ko = extendDictionary(en, { common: { conditions: { or: '또는', @@ -1807,6 +1808,6 @@ GitHub에 문의하거나 문제를 제출하기 전에 [docs.defguard.net](http `, }, }, -}; +}); export default ko; diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index a24fd757b..1f239406e 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -19,6 +19,7 @@ const pl: Translation = { copy: 'Skopiuj', rename: 'Zmień nazwę', edit: 'Edytuj', + dismiss: 'Odrzuć', }, conditions: { and: 'I', @@ -40,6 +41,22 @@ const pl: Translation = { insecureContext: 'Kontekst nie jest bezpieczny', }, modals: { + updatesNotification: { + header: { + criticalBadge: 'Aktualizacja krytyczna', + newVersion: 'Nowa wersja {version}', + title: 'Aktualizacja dostępna', + }, + controls: { + visitRelease: 'Zobacz stronę aktualizacji', + }, + }, + updatesNotificationToaster: { + title: 'Nowa wersja dostępna {version}', + controls: { + more: 'Zobacz co nowego', + }, + }, addGroup: { groupName: 'Nazwa grupy', searchPlaceholder: 'Szukaj', diff --git a/web/src/shared/components/Layout/RenderMarkdown/RenderMarkdown.tsx b/web/src/shared/components/Layout/RenderMarkdown/RenderMarkdown.tsx new file mode 100644 index 000000000..d8bfc4ac8 --- /dev/null +++ b/web/src/shared/components/Layout/RenderMarkdown/RenderMarkdown.tsx @@ -0,0 +1,26 @@ +import Markdown from 'react-markdown'; + +type Props = { + content: string; +}; +export const RenderMarkdown = ({ content }: Props) => { + const parse = (): string => { + const lines = content.split(/\r?\n/); + + const processedLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const isLastLine = i === lines.length - 1; + const currentLine = lines[i]; + + if (isLastLine && currentLine.trim() === '') { + processedLines.push(currentLine); + } else { + processedLines.push(currentLine.trim()); + } + } + + return processedLines.join('\n'); + }; + return {parse()}; +}; diff --git a/web/src/shared/components/Layout/VersionUpdateToast/VersionUpdateToast.tsx b/web/src/shared/components/Layout/VersionUpdateToast/VersionUpdateToast.tsx new file mode 100644 index 000000000..919849dde --- /dev/null +++ b/web/src/shared/components/Layout/VersionUpdateToast/VersionUpdateToast.tsx @@ -0,0 +1,113 @@ +import './style.scss'; + +import dayjs from 'dayjs'; +import { useCallback, useEffect } from 'react'; + +import { useI18nContext } from '../../../../i18n/i18n-react'; +import { ToastOptions } from '../../../defguard-ui/components/Layout/ToastManager/Toast/types'; +import { useToastsStore } from '../../../defguard-ui/hooks/toasts/useToastStore'; +import { useUpdatesStore } from '../../../hooks/store/useUpdatesStore'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const VersionUpdateToast = ({ id }: ToastOptions) => { + const removeToast = useToastsStore((s) => s.removeToast); + const updateData = useUpdatesStore((s) => s.update); + const dismissal = useUpdatesStore((s) => s.dismissal); + const setUpdateStore = useUpdatesStore((s) => s.setStore); + const { LL } = useI18nContext(); + + const closeToast = useCallback(() => { + removeToast(id); + }, [id, removeToast]); + + const handleOpenModal = () => { + setUpdateStore({ modalVisible: true }); + closeToast(); + }; + + const handleDismiss = () => { + if (updateData) { + setUpdateStore({ + dismissal: { + dismissedAt: dayjs.utc().toISOString(), + version: updateData.version, + }, + }); + closeToast(); + } + }; + + useEffect(() => { + if (dismissal && dismissal.version === updateData?.version) { + closeToast(); + } + }, [closeToast, dismissal, updateData?.version]); + + if (!updateData) return null; + + return ( +
+
+

+ {LL.modals.updatesNotificationToaster.title({ + version: updateData.version, + })} +

+ {updateData.critical && ( + + + + + )} + + + + + + +
+
+ + +
+
+ ); +}; diff --git a/web/src/shared/components/Layout/VersionUpdateToast/style.scss b/web/src/shared/components/Layout/VersionUpdateToast/style.scss new file mode 100644 index 000000000..a8f777be7 --- /dev/null +++ b/web/src/shared/components/Layout/VersionUpdateToast/style.scss @@ -0,0 +1,63 @@ +.update-toaster { + box-sizing: border-box; + padding: 10px 20px 15px; + box-shadow: 0px 6px 10px 0px rgba(0, 0, 0, 0.1); + border-radius: 10px; + background-color: var(--surface-nav-bg); + border-radius: 15px; + + min-width: 270px; + max-width: 100%; + + .top { + padding-bottom: 8px; + display: flex; + align-items: center; + flex-flow: row nowrap; + min-width: 230px; + column-gap: 8px; + + p { + color: var(--text-body-primary); + text-wrap: nowrap; + white-space: none; + @include typography(app-number); + } + + & > a { + margin-left: auto; + border: none; + background-color: transparent; + cursor: pointer; + width: 22px; + height: 22px; + display: inline-flex; + flex-flow: row; + align-content: center; + align-items: center; + justify-content: center; + padding: 4px; + box-sizing: border-box; + text-decoration: none; + } + } + + .bottom { + min-width: 230px; + display: flex; + flex-flow: row; + align-items: center; + justify-content: space-between; + + & > * { + cursor: pointer; + } + + button { + border: none; + background: transparent; + color: var(--text-body-primary); + @include typography(app-copyright); + } + } +} diff --git a/web/src/shared/components/modals/UpdateNotificationModal/UpdateNotificationModal.tsx b/web/src/shared/components/modals/UpdateNotificationModal/UpdateNotificationModal.tsx new file mode 100644 index 000000000..4646738b0 --- /dev/null +++ b/web/src/shared/components/modals/UpdateNotificationModal/UpdateNotificationModal.tsx @@ -0,0 +1,96 @@ +// eslint-disable-next-line simple-import-sort/imports +import { shallow } from 'zustand/shallow'; + +import { Modal } from '../../../defguard-ui/components/Layout/modals/Modal/Modal'; +import { useUpdatesStore } from '../../../hooks/store/useUpdatesStore'; +import { UpdateNotificationModalIcons } from './components/UpdateNotificationModalIcons'; +import { Button } from '../../../defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../defguard-ui/components/Layout/Button/types'; +import { useI18nContext } from '../../../../i18n/i18n-react'; +import { RenderMarkdown } from '../../Layout/RenderMarkdown/RenderMarkdown'; +import './style.scss'; +import dayjs from 'dayjs'; + +export const UpdateNotificationModal = () => { + const isOpen = useUpdatesStore((s) => s.modalVisible); + const close = useUpdatesStore((s) => s.closeModal, shallow); + + return ( + { + close(); + }} + className="updates-modal" + id="updates-modal" + disableClose + > + + + ); +}; + +const ModalContent = () => { + const { LL } = useI18nContext(); + const localLL = LL.modals.updatesNotification; + const data = useUpdatesStore((s) => s.update); + const setStore = useUpdatesStore((s) => s.setStore, shallow); + if (!data) return null; + return ( +
+
+
+ +

{localLL.header.title()}

+
+
+

+ {localLL.header.newVersion({ + version: data.version, + })} +

+ {data.critical && ( +
+ + {localLL.header.criticalBadge()} +
+ )} +
+
+
+
+ +
+
+
+
+
+ ); +}; diff --git a/web/src/shared/components/modals/UpdateNotificationModal/components/UpdateNotificationModalIcons.tsx b/web/src/shared/components/modals/UpdateNotificationModal/components/UpdateNotificationModalIcons.tsx new file mode 100644 index 000000000..7b3914e9d --- /dev/null +++ b/web/src/shared/components/modals/UpdateNotificationModal/components/UpdateNotificationModalIcons.tsx @@ -0,0 +1,62 @@ +type Icon = 'update' | 'alert'; + +type Props = { + variant: Icon; +}; + +export const UpdateNotificationModalIcons = ({ variant }: Props) => { + switch (variant) { + case 'alert': + return ( + + + + + ); + case 'update': + return ( + + + + + + ); + } +}; diff --git a/web/src/shared/components/modals/UpdateNotificationModal/style.scss b/web/src/shared/components/modals/UpdateNotificationModal/style.scss new file mode 100644 index 000000000..5906bb71e --- /dev/null +++ b/web/src/shared/components/modals/UpdateNotificationModal/style.scss @@ -0,0 +1,206 @@ +// modal content setup +#updates-modal { + max-width: 100%; + overflow-x: hidden; + + @include media-breakpoint-up(lg) { + background-color: transparent; + box-shadow: var(--box-shadow); + } + + .content-wrapper { + background-color: var(--surface-nav-bg); + border-radius: 0; + + @include media-breakpoint-down(lg) { + display: flex; + flex-flow: column; + align-items: center; + justify-content: flex-start; + width: 100%; + box-sizing: border-box; + padding: 20px 20px 40px; + row-gap: 30px; + } + + @include media-breakpoint-up(lg) { + background-color: var(--surface-main-primary); + border-radius: 15px; + } + + & > .top { + box-sizing: border-box; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + row-gap: 10px; + background-color: var(--surface-main-primary); + width: 100%; + padding: 20px; + + @include media-breakpoint-down(lg) { + border-radius: 15px; + min-width: 280px; + max-width: calc(100% - 40px); + } + + @include media-breakpoint-up(md) { + padding: 50px; + } + + @include media-breakpoint-up(lg) { + background-color: transparent; + padding: 56px 20px; + border-top-left-radius: 15px; + border-top-right-radius: 15px; + } + + & > div { + display: flex; + flex-flow: row; + align-items: center; + } + + .header { + column-gap: 20px; + + svg { + width: 30px; + height: 30px; + + @include media-breakpoint-up(lg) { + width: 50px; + height: 50px; + } + } + + p { + color: var(--surface-icon-secondary); + + text-wrap: nowrap; + + @include media-breakpoint-down(md) { + font-size: 24px; + } + + @include typography(app-title); + } + } + + .info { + column-gap: 8px; + + @include media-breakpoint-down(md) { + display: flex; + flex-flow: column; + row-gap: 10px; + } + + .version { + color: var(--surface-icon-secondary); + + @include media-breakpoint-down(md) { + font-size: 16px; + } + + @include typography(app-welcome-2); + } + + .badge { + display: flex; + flex-flow: row; + column-gap: 8px; + box-sizing: border-box; + padding: 5px 20px; + border-radius: 5px; + background-color: var(--surface-nav-bg); + align-items: center; + justify-content: center; + min-height: 25px; + color: var(--surface-alert-primary); + + span, + p { + @include typography(app-body-2); + } + } + } + } + + & > .bottom { + background-color: var(--surface-nav-bg); + + @include media-breakpoint-up(lg) { + border-radius: 15px; + padding: 30px 50px 50px; + } + + & > .content { + padding-bottom: 40px; + display: flex; + flex-flow: column; + row-gap: 20px; + + ul { + padding-left: 20px; + box-sizing: border-box; + } + + p, + span, + a, + li { + @include typography(app-body-2); + } + + h1 { + @include typography(app-welcome-1); + } + + h2 { + @include typography(markdown-h4); + } + + h3 { + @include typography(markdown-h5); + } + } + + .controls { + width: 100%; + display: flex; + gap: 20px; + flex-flow: column-reverse; + + @include media-breakpoint-up(md) { + flex-flow: row; + } + + .btn, + a { + width: 100%; + text-decoration: none; + } + + .btn { + max-height: 47px; + } + } + } + } +} + +// modal container setup +.modal-root { + .modal.updates-modal { + min-height: 100dvh; + + @include media-breakpoint-up(lg) { + grid-template-columns: max(920px); + padding-top: 200px; + justify-content: center; + align-items: start; + } + } +} diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index b61bef8c8..ad1f34408 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit b61bef8c893b4a27f62a3463d847274591520398 +Subproject commit ad1f34408f449f88e08c2fd8128d78b38ca9e46e diff --git a/web/src/shared/hooks/store/useUpdatesStore.tsx b/web/src/shared/hooks/store/useUpdatesStore.tsx new file mode 100644 index 000000000..8a3e749aa --- /dev/null +++ b/web/src/shared/hooks/store/useUpdatesStore.tsx @@ -0,0 +1,75 @@ +import { isObject, pick } from 'lodash-es'; +import { persist } from 'zustand/middleware'; +import { createWithEqualityFn } from 'zustand/traditional'; + +import { VersionUpdateToast } from '../../components/Layout/VersionUpdateToast/VersionUpdateToast'; +import { ToastType } from '../../defguard-ui/components/Layout/ToastManager/Toast/types'; +import { useToastsStore } from '../../defguard-ui/hooks/toasts/useToastStore'; + +const keysToPersist: Array = ['dismissal']; + +const defaultState: StoreValues = { + modalVisible: false, + dismissal: undefined, + update: undefined, +}; + +export const useUpdatesStore = createWithEqualityFn()( + persist( + (set, get) => ({ + ...defaultState, + setStore: (vals) => set(vals), + openModal: () => set({ modalVisible: true }), + closeModal: () => set({ modalVisible: false }), + setUpdate: (update) => { + const state = get(); + if (!state.dismissal || state.dismissal.version !== update.version) { + useToastsStore.getState().addToast({ + customComponent: VersionUpdateToast, + message: '', + type: ToastType.INFO, + }); + set({ update: update }); + } else { + set({ update: update }); + } + }, + clearUpdate: () => set({ update: undefined }), + }), + { + name: 'updates-store', + version: 1, + partialize: (s) => pick(s, keysToPersist), + }, + ), + isObject, +); + +type Store = StoreValues & StoreMethods; + +type Dismissal = { + version: string; + dismissedAt: string; +}; + +export type UpdateInfo = { + version: string; + critical: boolean; + // Markdown + notes: string; + release_notes_url: string; +}; + +type StoreValues = { + modalVisible: boolean; + dismissal?: Dismissal; + update?: UpdateInfo; +}; + +type StoreMethods = { + setStore: (values: Partial) => void; + openModal: () => void; + closeModal: () => void; + setUpdate: (value: NonNullable) => void; + clearUpdate: () => void; +}; diff --git a/web/src/shared/hooks/useApi.tsx b/web/src/shared/hooks/useApi.tsx index 14b8bcb01..1cd5307d9 100644 --- a/web/src/shared/hooks/useApi.tsx +++ b/web/src/shared/hooks/useApi.tsx @@ -476,6 +476,14 @@ const useApi = (props?: HookProps): ApiHook => { return {}; }); + const getNewVersion: ApiHook['getNewVersion'] = () => + client.get('/updates').then((res) => { + if (res.status === 204) { + return null; + } + return res.data; + }); + useEffect(() => { client.interceptors.response.use( (res) => { @@ -504,6 +512,7 @@ const useApi = (props?: HookProps): ApiHook => { return { getAppInfo, + getNewVersion, changePasswordSelf, getEnterpriseStatus, getEnterpriseInfo, diff --git a/web/src/shared/queries.ts b/web/src/shared/queries.ts index c930639cd..f4d3c6459 100644 --- a/web/src/shared/queries.ts +++ b/web/src/shared/queries.ts @@ -31,4 +31,5 @@ export const QueryKeys = { FETCH_ENTERPRISE_STATUS: 'FETCH_ENTERPRISE_STATUS', FETCH_ENTERPRISE_SETTINGS: 'FETCH_ENTERPRISE_SETTINGS', FETCH_ENTERPRISE_INFO: 'FETCH_ENTERPRISE_INFO', + FETCH_NEW_VERSION: 'FETCH_NEW_VERSION', }; diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 27f00ebf2..418f3716f 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -6,6 +6,8 @@ import { } from '@github/webauthn-json'; import { AxiosError, AxiosPromise } from 'axios'; +import { UpdateInfo } from './hooks/store/useUpdatesStore'; + export type ApiError = AxiosError; export type ApiErrorResponse = { @@ -434,6 +436,7 @@ export type AuthenticationKey = { export interface ApiHook { getAppInfo: () => Promise; + getNewVersion: () => Promise; changePasswordSelf: (data: ChangePasswordSelfRequest) => Promise; getEnterpriseStatus: () => Promise; getEnterpriseInfo: () => Promise; diff --git a/web/src/shared/validators.ts b/web/src/shared/validators.ts index 2e91b17df..b935329d2 100644 --- a/web/src/shared/validators.ts +++ b/web/src/shared/validators.ts @@ -38,8 +38,8 @@ export const validateIpOrDomainList = ( allowMasks = false, allowIPv6 = false, ): boolean => { - const trimed = val.replace(' ', ''); - const split = trimed.split(splitWith); + const trimmed = val.replace(' ', ''); + const split = trimmed.split(splitWith); for (const value of split) { if ( !validateIPv4(value, allowMasks) && @@ -52,7 +52,7 @@ export const validateIpOrDomainList = ( return true; }; -// Returns flase when invalid +// Returns false when invalid export const validateIPv4 = (ip: string, allowMask = false): boolean => { if (allowMask) { if (ip.includes('/')) { diff --git a/web/vite.config.mts b/web/vite.config.mts index bcabb2ba0..6d699f994 100644 --- a/web/vite.config.mts +++ b/web/vite.config.mts @@ -1,57 +1,74 @@ +import 'dotenv/config'; + import react from '@vitejs/plugin-react-swc'; import autoprefixer from 'autoprefixer'; import * as path from 'path'; import { defineConfig } from 'vite'; -export default defineConfig({ - clearScreen: false, - plugins: [react()], - server: { - strictPort: false, - port: 3000, - proxy: { - '/api': { - target: 'http://127.0.0.1:8000/', - changeOrigin: true, - }, - '/.well-known': { - target: 'http://127.0.0.1:8000/', - changeOrigin: true, +// eslint-disable-next-line no-empty-pattern +export default ({}) => { + let proxyTarget = 'http://127.0.0.1:8000'; + const envProxyTarget = process.env.PROXY_TARGET; + + if (envProxyTarget && envProxyTarget.length > 0) { + proxyTarget = envProxyTarget; + } + + return defineConfig({ + clearScreen: false, + plugins: [react()], + server: { + strictPort: false, + port: 3000, + cors: true, + proxy: { + '/api': { + target: proxyTarget, + changeOrigin: true, + secure: false, + }, + '/.well-known': { + target: proxyTarget, + changeOrigin: true, + secure: false, + }, + '/svg': { + target: proxyTarget, + changeOrigin: true, + secure: false, + }, }, - '/svg': { - target: 'http://127.0.0.1:8000/', - changeOrigin: true, + fs: { + allow: ['.'], }, }, - fs: { - allow: ['.'], - }, - }, - envPrefix: ['VITE_'], - assetsInclude: ['./src/shared/assets/**/*'], - resolve: { - alias: { - '@scss': path.resolve(__dirname, '/src/shared/scss'), - '@scssutils': path.resolve(__dirname, '/src/shared/scss/global'), - }, - }, - build: { - chunkSizeWarningLimit: 10000, - rollupOptions: { - logLevel: 'silent', - onwarn: (warning, warn) => { - return; + envPrefix: ['VITE_'], + assetsInclude: ['./src/shared/assets/**/*'], + resolve: { + alias: { + '@scss': path.resolve(__dirname, './src/shared/scss'), + '@scssutils': path.resolve(__dirname, './src/shared/scss/global'), }, }, - }, - css: { - preprocessorOptions: { - scss: { - additionalData: `@use "@scssutils" as *;\n`, + build: { + chunkSizeWarningLimit: 10000, + rollupOptions: { + logLevel: 'silent', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onwarn: (_warning, _warn) => { + return; + }, }, }, - postcss: { - plugins: [autoprefixer], + css: { + preprocessorOptions: { + scss: { + additionalData: `@use "@scssutils" as *;\n`, + }, + }, + postcss: { + plugins: [autoprefixer], + }, }, - }, -}); + }); +};