diff --git a/.sqlx/query-60a87e9d4f92298692c0a978f2077f7967909c3b13677177a5a0c87b6d07c0aa.json b/.sqlx/query-60a87e9d4f92298692c0a978f2077f7967909c3b13677177a5a0c87b6d07c0aa.json new file mode 100644 index 0000000000..c2410566cd --- /dev/null +++ b/.sqlx/query-60a87e9d4f92298692c0a978f2077f7967909c3b13677177a5a0c87b6d07c0aa.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM wireguard_network", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "60a87e9d4f92298692c0a978f2077f7967909c3b13677177a5a0c87b6d07c0aa" +} diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index b78bd38aaf..5b11da0308 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -213,6 +213,18 @@ pub enum NetworkAddressError { } impl WireguardNetwork { + pub async fn count<'e, E>(executor: E) -> sqlx::Result + where + E: PgExecutor<'e>, + { + let count = query_scalar!("SELECT COUNT(*) FROM wireguard_network") + .fetch_one(executor) + .await? + .unwrap_or_default(); + + Ok(count as usize) + } + #[allow(clippy::too_many_arguments)] #[must_use] pub fn new( diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index d3e02bdf27..ca1f096703 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -53,6 +53,11 @@ pub(crate) struct WireguardNetworkInfo { has_devices: bool, } +#[derive(Serialize, ToSchema)] +pub(crate) struct LocationsCount { + count: usize, +} + #[derive(Deserialize, Serialize, ToSchema)] pub struct WireguardNetworkData { pub name: String, @@ -511,6 +516,34 @@ pub async fn list_networks(_role: AdminRole, State(appstate): State) - Ok(ApiResponse::json(network_info, StatusCode::OK)) } +/// Number of all networks +/// +/// Retrieve count of all networks. +/// +/// # Returns +/// - `LocationsCount` object +/// +/// - `WebError` if error occurs +#[utoipa::path( + get, + path = "/api/v1/network/count", + responses( + (status = 200, description = "Count of all networks", body = LocationsCount), + (status = 401, description = "Unauthorized to count networks.", body = ApiResponse, example = json!({"msg": "Session is required"})), + (status = 403, description = "You don't have permission to count networks.", body = ApiResponse, example = json!({"msg": "access denied"})), + (status = 500, description = "Unable to count networks.", body = ApiResponse, example = json!({"msg": "Internal server error"})) + ), + security( + ("cookie" = []), + ("api_token" = []) + ) +)] +pub async fn count_networks(_role: AdminRole, State(appstate): State) -> ApiResult { + debug!("Counting WireGuard networks"); + let count = WireguardNetwork::count(&appstate.pool).await?; + Ok(ApiResponse::json(LocationsCount { count }, StatusCode::OK)) +} + /// Details of network /// /// Retrieve details about network with `network_id`. diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 4679d7f2d1..0100b0d301 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -163,9 +163,10 @@ use crate::{ add_webhook, change_enabled, change_webhook, delete_webhook, get_webhook, list_webhooks, }, wireguard::{ - add_device, add_user_devices, create_network, delete_device, delete_network, - download_config, gateway_status, get_device, import_network, list_devices, - list_networks, list_user_devices, modify_device, modify_network, network_details, + add_device, add_user_devices, count_networks, create_network, delete_device, + delete_network, download_config, gateway_status, get_device, import_network, + list_devices, list_networks, list_user_devices, modify_device, modify_network, + network_details, }, worker::{create_job, create_worker_token, job_status, list_workers, remove_worker}, }, @@ -537,6 +538,7 @@ pub fn build_webapp( post(start_network_device_setup_for_device), ) .route("/network", post(create_network).get(list_networks)) + .route("/network/count", get(count_networks)) .route("/network/display", get(get_locations_display)) .route("/network/import", post(import_network)) .route("/network/stats", get(locations_overview_stats)) diff --git a/crates/defguard_core/src/openapi.rs b/crates/defguard_core/src/openapi.rs index 5c3483856a..4340b7caa9 100644 --- a/crates/defguard_core/src/openapi.rs +++ b/crates/defguard_core/src/openapi.rs @@ -71,6 +71,7 @@ use super::{ network::modify_network, network::delete_network, network::list_networks, + network::count_networks, network::network_details, // /license license::license_check, diff --git a/crates/defguard_setup/src/handlers/migration.rs b/crates/defguard_setup/src/handlers/migration.rs index 220a5f815d..8bef3106b0 100644 --- a/crates/defguard_setup/src/handlers/migration.rs +++ b/crates/defguard_setup/src/handlers/migration.rs @@ -1,10 +1,7 @@ use std::sync::{Arc, Mutex}; use axum::{Extension, Json}; -use defguard_common::db::models::{ - ActiveWizard, Settings, Wizard, group::Group, migration_wizard::MigrationWizardState, - settings::update_current_settings, -}; +use defguard_common::db::models::{ActiveWizard, Wizard, migration_wizard::MigrationWizardState}; use defguard_core::{ auth::AdminOrSetupRole, error::WebError, @@ -15,7 +12,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; use tokio::sync::oneshot; -use tracing::{debug, info}; +use tracing::info; pub async fn get_migration_state( _: AdminOrSetupRole, @@ -38,66 +35,10 @@ pub async fn update_migration_state( #[derive(Deserialize, Serialize, Debug)] pub struct GeneralConfig { defguard_url: String, - default_admin_group_name: String, - default_authentication: u32, default_mfa_code_lifetime: u32, public_proxy_url: String, } -pub async fn set_general_config( - _: AdminOrSetupRole, - Extension(pool): Extension, - Json(general_config): Json, -) -> ApiResult { - info!("Applying initial general configuration settings"); - debug!( - "General configuration received: defguard_url={}, default_admin_group_name={}, default_authentication={}, default_mfa_code_lifetime={}, public_proxy_url={}", - general_config.defguard_url, - general_config.default_admin_group_name, - general_config.default_authentication, - general_config.default_mfa_code_lifetime, - general_config.public_proxy_url, - ); - let default_admin_group_name = general_config.default_admin_group_name.clone(); - let mut settings = Settings::get_current_settings(); - settings.public_proxy_url = general_config.public_proxy_url; - settings.defguard_url = general_config.defguard_url; - settings.default_admin_group_name = general_config.default_admin_group_name; - settings.authentication_period_days = general_config - .default_authentication - .try_into() - .map_err(|err| { - WebError::BadRequest(format!("Invalid authentication period days: {err}")) - })?; - settings.mfa_code_timeout_seconds = general_config - .default_mfa_code_lifetime - .try_into() - .map_err(|err| WebError::BadRequest(format!("Invalid MFA code timeout seconds: {err}")))?; - update_current_settings(&pool, settings).await?; - debug!("Settings persisted"); - - if let Some(mut group) = Group::find_by_name(&pool, &default_admin_group_name).await? { - debug!( - "Admin group {} found, marking as admin", - default_admin_group_name - ); - group.is_admin = true; - group.save(&pool).await?; - } else { - debug!( - "Admin group {} not found, creating", - default_admin_group_name - ); - let mut group = Group::new(&default_admin_group_name); - group.is_admin = true; - group.save(&pool).await?; - } - - info!("Initial general configuration applied"); - - Ok(ApiResponse::with_status(StatusCode::OK)) -} - pub async fn finish_setup( _: AdminOrSetupRole, Extension(pool): Extension, diff --git a/crates/defguard_setup/src/migration.rs b/crates/defguard_setup/src/migration.rs index ec98119da0..acdc86d0f6 100644 --- a/crates/defguard_setup/src/migration.rs +++ b/crates/defguard_setup/src/migration.rs @@ -29,7 +29,7 @@ use defguard_core::{ resource_display::get_locations_display, session_info::get_session_info, settings::{get_settings, get_settings_essentials, patch_settings}, - wireguard::list_networks, + wireguard::{count_networks, list_networks}, }, health_check, version::IncompatibleComponents, @@ -45,7 +45,7 @@ use tracing::{info, instrument}; use crate::handlers::{ initial_wizard::{create_ca, get_ca, upload_ca}, - migration::{finish_setup, get_migration_state, set_general_config, update_migration_state}, + migration::{finish_setup, get_migration_state, update_migration_state}, }; /// FIXME: This is a workaround which enables us to reuse the same API handlers @@ -118,6 +118,7 @@ pub fn build_migration_webapp( .route("/auth/email/verify", post(email_mfa_code)) .route("/auth/recovery", post(recovery_code)) .route("/network", get(list_networks)) + .route("/network/count", get(count_networks)) .route("/network/display", get(get_locations_display)) .route( "/network/{network_id}/gateways/setup", @@ -130,7 +131,6 @@ pub fn build_migration_webapp( "/state", get(get_migration_state).put(update_migration_state), ) - .route("/general_config", post(set_general_config)) .route("/ca", post(create_ca).get(get_ca)) .route("/ca/upload", post(upload_ca)) .route("/finish", post(finish_setup)), diff --git a/crates/defguard_setup/tests/migration_wizard.rs b/crates/defguard_setup/tests/migration_wizard.rs index b4faff3e8c..e1903f1dfe 100644 --- a/crates/defguard_setup/tests/migration_wizard.rs +++ b/crates/defguard_setup/tests/migration_wizard.rs @@ -97,17 +97,16 @@ async fn test_migration_full_flow(_: PgPoolOptions, options: PgConnectOptions) { assert_migration_step(&pool, "general").await; let resp = client - .post("/api/v1/migration/general_config") + .client + .patch(format!("{}/api/v1/settings", client.base_url())) .json(&json!({ "defguard_url": "https://migration.example.com", - "default_admin_group_name": "admins", - "default_authentication": 14, - "default_mfa_code_lifetime": 120, - "public_proxy_url": "https://proxy.migration.example.com" + "authentication_period_days": 14, + "mfa_code_timeout_seconds": 120 })) .send() .await - .expect("Failed to POST /api/v1/migration/general_config"); + .expect("Failed to PATCH /api/v1/settings"); assert_eq!(resp.status(), StatusCode::OK); let settings = Settings::get(&pool) @@ -225,22 +224,20 @@ async fn test_migration_auth_enforcement(_: PgPoolOptions, options: PgConnectOpt ); let resp = unauth - .post(format!("{base}/api/v1/migration/general_config")) + .patch(format!("{base}/api/v1/settings")) .header(USER_AGENT, "test/0.0") .json(&json!({ "defguard_url": "https://x.example.com", - "default_admin_group_name": "admins", - "default_authentication": 14, - "default_mfa_code_lifetime": 120, - "public_proxy_url": "https://px.example.com" + "authentication_period_days": 14, + "mfa_code_timeout_seconds": 120 })) .send() .await - .expect("Failed POST migration/general_config"); + .expect("Failed PATCH settings"); assert_eq!( resp.status(), StatusCode::UNAUTHORIZED, - "POST /migration/general_config should require auth" + "PATCH /settings should require auth" ); let resp = unauth diff --git a/web/index.html b/web/index.html index 54b6ba3730..e92f5c7104 100644 --- a/web/index.html +++ b/web/index.html @@ -8,23 +8,23 @@ Defguard - - - - + + + + - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/web/messages/en/edge_wizard.json b/web/messages/en/edge_wizard.json index 2b4d81c99e..5ff7deb51d 100644 --- a/web/messages/en/edge_wizard.json +++ b/web/messages/en/edge_wizard.json @@ -21,10 +21,13 @@ "edge_setup_step_edge_adoption_description": "Review the system's checks and see if any issues need attention before deployment.", "edge_setup_step_confirmation_label": "Confirmation", "edge_setup_step_confirmation_description": "Your configuration was successful. You're all set.", - "edge_setup_component_label_common_name": "Common Name", + "edge_setup_component_label_common_name": "Edge Name", + "edge_setup_component_error_common_name_help": "This name will be visible in the Edge Components list and is used to identify a deployed instance, as there may be multiple instances for High Availability (HA).", "edge_setup_component_label_ip_or_domain": "IP or Domain", + "edge_setup_component_label_ip_or_domain_help": "Enter the IP or domain name of the server where the Edge Component is deployed. Core will then connect to it and perform the adoption automatically.", "edge_setup_component_label_grpc_port": "gRPC Port", - "edge_setup_component_error_common_name_required": "Common Name is required", + "edge_setup_component_label_grpc_port_help": "Specify the gRPC TCP port for Edge Component communication. If unchanged, leave the default value. Ensure this port is open and accessible to the Core instance through any server or network firewalls.", + "edge_setup_component_error_common_name_required": "Edge Name is required", "edge_setup_component_error_ip_or_domain_required": "IP or Domain is required", "edge_setup_component_error_grpc_port_required": "gRPC Port is required", "edge_setup_component_error_grpc_port_max": "gRPC Port must be less than 65536", diff --git a/web/messages/en/initial_wizard.json b/web/messages/en/initial_wizard.json index f9165635f1..4712430442 100644 --- a/web/messages/en/initial_wizard.json +++ b/web/messages/en/initial_wizard.json @@ -145,9 +145,11 @@ "initial_setup_ca_error_upload_failed": "Failed to upload CA. Please ensure the certificate file is valid and try again.", "initial_setup_ca_option_create_title": "Create a certificate authority & configure all Defguard components", "initial_setup_ca_option_create_description": "By choosing this option, Defguard will create its own certificate authority and automatically configure all components to use its certificates — no manual setup required.", - "initial_setup_ca_label_common_name": "Common Name", + "initial_setup_ca_label_common_name": "Certificate Authority Name", + "initial_setup_ca_helper_common_name": "Can be any name you wish.", "initial_setup_ca_placeholder_common_name": "Defguard Certificate Authority", - "initial_setup_ca_label_email": "Email", + "initial_setup_ca_label_email": "Certificate Authority Email", + "initial_setup_ca_helper_email": "Each certificate authority (or any certificate) has a contact email field. Can be your email or general IT email.", "initial_setup_ca_placeholder_email": "email@example.com", "initial_setup_ca_label_validity": "Validity Period", "initial_setup_ca_option_use_own_title": "Use your own certificate authority", diff --git a/web/messages/en/migration_wizard.json b/web/messages/en/migration_wizard.json index 9712b45e5a..4e4d9209ce 100644 --- a/web/messages/en/migration_wizard.json +++ b/web/messages/en/migration_wizard.json @@ -2,18 +2,30 @@ "$schema": "https://inlang.com/schema/inlang-message-format", "migration_wizard_title": "Migration Wizard", "migration_wizard_subtitle": "This wizard will guide you through migration-related configuration of your Defguard instance.", + "migration_wizard_welcome_title": "Welcome to Defguard Migration Wizard.", + "migration_wizard_welcome_subtitle": "We've detected your previous version with {count} locations.", + "migration_wizard_welcome_docs_text": "We'll guide you through the process step by step. For full details, see the migration guide following the link below.", + "migration_wizard_start_warning": "IMPORTANT: Until you finish this migration process your VPN locations will not work.", + "migration_wizard_start_explain_1": "We will first automatically upgrade the Core instance (what you see now), followed by the public communication component, Edge (called Proxy prior to 2.0).", + "migration_wizard_start_explain_2": "Next, each VPN location must be upgraded. This will likely require manual changes to your internal network (firewall rules), as the Core ↔ Gateway communication has changed: the Core now initiates the connection to the Gateway (in 1.x Gateway connected to Core).", + "migration_wizard_start_button": "Start migration process", "migration_wizard_step_general_config_label": "General Configuration", "migration_wizard_step_general_config_description": "Manage core details and connection parameters for your VPN location.", "migration_wizard_step_certificate_authority_label": "Certificate Authority", - "migration_wizard_step_certificate_authority_description": "Securing component communication", + "migration_wizard_step_certificate_authority_description": "We have incorporated a Certificate Authority (CA) management into Defguard to simplify component deployment and management.", "migration_wizard_step_certificate_authority_summary_label": "Certificate Authority Summary", "migration_wizard_step_certificate_authority_summary_description": "Securing component communication", "migration_wizard_step_edge_component_label": "Edge Component", - "migration_wizard_step_edge_component_description": "Set up your VPN proxy quickly and ensure secure, optimized traffic flow for your users.", + "migration_wizard_step_edge_component_description": "Starting with Defguard 2.0, Proxy has been renamed to Edge, and support for multiple instances (high availability) has been introduced. ", "migration_wizard_step_edge_adoption_label": "Edge Component Adoption", "migration_wizard_step_edge_adoption_description": "Review the system's checks and see if any issues need attention before deployment.", "migration_wizard_step_confirmation_label": "Confirmation", "migration_wizard_step_confirmation_description": "Your configuration was successful. You're all set.", + "migration_wizard_general_config_private_url_title": "Private URL", + "migration_wizard_general_config_private_url_description": "This URL will be used to access and control Defguard. It should not be exposed to the Internet only to the internal or VPN network. You can learn more about our security approach in the video below.", + "migration_wizard_general_config_public_url_title": "Public URL", + "migration_wizard_general_config_public_url_description": "This URL will be used to access and control Defguard. It should not be exposed to the Internet only to the internal or VPN network. You can learn more about our security approach in the video below.", + "migration_wizard_edge_component_info": "We've detected your current Proxy setup. Please upgrade it to the latest 2.0 Edge component so it can be adopted automatically and managed by Defguard.", "migration_wizard_confirmation_title": "Initial system migration are complete.", "migration_wizard_confirmation_subtitle": "You've completed the first stage of the migration. Defguard is almost ready to go.", "migration_wizard_confirmation_locations_info": "You currently have {count} VPN locations configured. These locations must be upgraded.", @@ -24,22 +36,25 @@ "migration_wizard_confirmation_prepare_network_title": "Prepare your network", "migration_wizard_confirmation_prepare_network_subtitle": "Please prepare all required network and firewall changes before starting the migration. Once ready, we'll begin adopting the upgraded gateway components for each VPN location.", "migration_wizard_confirmation_checkbox_label": "I have changed all my gateways firewall rules and network setup", + "migration_wizard_finish_success_snackbar": "Migration finished", + "migration_wizard_finish_error_snackbar": "Finishing migration failed", "migration_wizard_ca_validity_one_year": "1 year", "migration_wizard_ca_validity_years": "{years} years", "migration_wizard_general_config_error_invalid_url": "Invalid URL", - "migration_wizard_general_config_error_defguard_url_required": "Defguard URL is required", - "migration_wizard_general_config_error_defguard_url_invalid_host": "Defguard URL must use a hostname, not an IP address", + "migration_wizard_general_config_error_defguard_url_required": "Defguard Private URL is required", + "migration_wizard_general_config_error_defguard_url_invalid_host": "Defguard Private URL must use a hostname, not an IP address", "migration_wizard_general_config_error_admin_group_required": "Default admin group name is required", "migration_wizard_general_config_error_auth_period_min": "Authentication period must be at least 1 day", "migration_wizard_general_config_error_mfa_timeout_min": "MFA code timeout must be at least 60 seconds", - "migration_wizard_general_config_label_defguard_url": "Defguard URL", + "migration_wizard_general_config_label_defguard_url": "Defguard Private URL", "migration_wizard_general_config_label_admin_group": "Default Admin Group Name", "migration_wizard_general_config_label_auth_period": "Default Authentication Period (days)", "migration_wizard_general_config_label_mfa_timeout": "Default MFA Code Timeout (seconds)", - "migration_wizard_general_config_label_public_proxy_url": "Public Edge component URL", - "migration_wizard_general_config_error_public_proxy_url_invalid": "Public Proxy URL must be a valid URL", - "migration_wizard_general_config_error_public_proxy_url_required": "Public Proxy URL is required", - "migration_wizard_ca_error_common_name_required": "Common name is required", + "migration_wizard_general_config_label_public_proxy_url": "Defguard Public URL", + "migration_wizard_general_config_label_help_public_proxy_url": "", + "migration_wizard_general_config_error_public_proxy_url_invalid": "Defguard Public URL must be a valid URL", + "migration_wizard_general_config_error_public_proxy_url_required": "Defguard Public URL is required", + "migration_wizard_ca_error_common_name_required": "Certificate Authority Name is required", "migration_wizard_ca_error_email_invalid": "Invalid email address", "migration_wizard_ca_error_email_required": "Email is required", "migration_wizard_ca_error_validity_min": "Validity period must be at least 1 year", @@ -48,9 +63,11 @@ "migration_wizard_ca_error_upload_failed": "Failed to upload CA. Please ensure the certificate file is valid and try again.", "migration_wizard_ca_option_create_title": "Create a certificate authority & configure all Defguard components", "migration_wizard_ca_option_create_description": "By choosing this option, Defguard will create its own certificate authority and automatically configure all components to use its certificates — no manual setup required.", - "migration_wizard_ca_label_common_name": "Common Name", + "migration_wizard_ca_label_common_name": "Certificate Authority Name", + "migration_wizard_ca_helper_common_name": "Can be any name you wish.", "migration_wizard_ca_placeholder_common_name": "Defguard Certificate Authority", - "migration_wizard_ca_label_email": "Email", + "migration_wizard_ca_label_email": "Certificate Authority Email", + "migration_wizard_ca_helper_email": "Each certificate authority (or any certificate) has a contact email field. Can be your email or general IT email.", "migration_wizard_ca_placeholder_email": "email@example.com", "migration_wizard_ca_label_validity": "Validity Period", "migration_wizard_ca_generated_title": "Certificate Authority Generated", diff --git a/web/package.json b/web/package.json index 25be4800eb..34e6e8fcba 100644 --- a/web/package.json +++ b/web/package.json @@ -22,7 +22,7 @@ "@stablelib/x25519": "^2.0.1", "@tanstack/react-form": "^1.28.5", "@tanstack/react-query": "^5.90.21", - "@tanstack/react-router": "^1.167.3", + "@tanstack/react-router": "^1.167.4", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.23", "@uidotdev/usehooks": "^2.4.1", @@ -34,7 +34,7 @@ "humanize-duration": "^3.33.2", "ipaddr.js": "^2.3.0", "lodash-es": "^4.17.23", - "motion": "^12.37.0", + "motion": "^12.38.0", "qrcode.react": "^4.2.0", "qs": "^6.15.0", "radashi": "^12.7.2", @@ -57,7 +57,7 @@ "@tanstack/react-devtools": "^0.10.0", "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router-devtools": "^1.166.9", - "@tanstack/router-plugin": "^1.166.12", + "@tanstack/router-plugin": "^1.166.13", "@types/byte-size": "^8.1.2", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index c2348267d7..8246963e53 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^5.90.21 version: 5.90.21(react@19.2.4) '@tanstack/react-router': - specifier: ^1.167.3 - version: 1.167.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^1.167.4 + version: 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -72,8 +72,8 @@ importers: specifier: ^4.17.23 version: 4.17.23 motion: - specifier: ^12.37.0 - version: 12.37.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^12.38.0 + version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) qrcode.react: specifier: ^4.2.0 version: 4.2.0(react@19.2.4) @@ -131,10 +131,10 @@ importers: version: 5.91.3(@tanstack/react-query@5.90.21(react@19.2.4))(react@19.2.4) '@tanstack/react-router-devtools': specifier: ^1.166.9 - version: 1.166.9(@tanstack/react-router@1.167.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.166.9(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/router-plugin': - specifier: ^1.166.12 - version: 1.166.12(@tanstack/react-router@1.167.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + specifier: ^1.166.13 + version: 1.166.13(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -251,12 +251,12 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true @@ -1175,8 +1175,8 @@ packages: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.167.3': - resolution: {integrity: sha512-1qbSy4r+O7IBdmPLlcKsjB041Gq2MMnIEAYSGIjaMZIL4duUIQnOWLw4jTfjKil/IJz/9rO5JcvrbxOG5UTSdg==} + '@tanstack/react-router@1.167.4': + resolution: {integrity: sha512-VpbZh382zX3WF4+X2Z+EUyd8eJhJyjg9C6ByYwrVZiWbhgbMK4+zQQIG2+lCAlIlDi7SV8fDcGL09NA8Z2kpGQ==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1201,9 +1201,10 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.167.3': - resolution: {integrity: sha512-M/CxrTGKk1fsySJjd+Pzpbi3YLDz+cJSutDjSTMy12owWlOgHV/I6kzR0UxyaBlHraM6XgMHNA0XdgsS1fa4Nw==} + '@tanstack/router-core@1.167.4': + resolution: {integrity: sha512-Gk5V9Zr5JFJ4SbLyCheQLJ3MnXddccENPA+DJRz+9g3QxtN8DJB8w8KCUCgDeYlWp4LvmO4nX3fy3tupqVP2Pw==} engines: {node: '>=20.19'} + hasBin: true '@tanstack/router-devtools-core@1.166.9': resolution: {integrity: sha512-PNlA7GmOUX9wY7LUG709Pk3Lg33dfHBztQwzjzrOiOsuf4ggp2R6bwarF8nYGNjG79z/MaB5PN+5yvkCVk8jGw==} @@ -1215,16 +1216,17 @@ packages: csstype: optional: true - '@tanstack/router-generator@1.166.11': - resolution: {integrity: sha512-Q/49wxURbft1oNOvo/eVAWZq/lNLK3nBGlavqhLToAYXY6LCzfMtRlE/y3XPHzYC9pZc09u5jvBR1k1E4hyGDQ==} + '@tanstack/router-generator@1.166.12': + resolution: {integrity: sha512-2HdxSTbCkbU9JeYogKVigIlXoLtIJE1x5rbEov+ZLTPjGCO9kicNQuljqg9Js+u2/ahtWewNrE5u1QCAyxmpIg==} engines: {node: '>=20.19'} - '@tanstack/router-plugin@1.166.12': - resolution: {integrity: sha512-PYsnN6goK6zBaVo63UVKjofv69+HHMKRQXymwN55JYKguNnNR8OZ6E12icPb0Olc5uIpPiGz1YI2+rbpmNKGHA==} + '@tanstack/router-plugin@1.166.13': + resolution: {integrity: sha512-xG3ND3AlMe6DN9PihJAYUbQJevqJvVdzN1QpZbfU1/jkHurL97ynP2yXfmMTh8Qgi1K+SWRko4bi7iZlYP9SUw==} engines: {node: '>=20.19'} + hasBin: true peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.167.3 + '@tanstack/react-router': ^1.167.4 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' vite-plugin-solid: ^2.11.10 webpack: '>=5.92.0' @@ -1254,9 +1256,10 @@ packages: '@tanstack/virtual-core@3.13.23': resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} - '@tanstack/virtual-file-routes@1.161.6': - resolution: {integrity: sha512-EGWs9yvJA821pUkwkiZLQW89CzUumHyJy8NKq229BubyoWXfDw1oWnTJYSS/hhbLiwP9+KpopjeF5wWwnCCyeQ==} + '@tanstack/virtual-file-routes@1.161.7': + resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==} engines: {node: '>=20.19'} + hasBin: true '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1471,8 +1474,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001779: - resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==} + caniuse-lite@1.0.30001780: + resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1775,8 +1778,8 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.37.0: - resolution: {integrity: sha512-j/QUcZS9Nw3NzZWoAbkzr3ETRFHyVeQMlGOUYUmG15U+uiyn9DqIktYruVPDcqY8I35qYR70JaZBvFmS6p+Pdg==} + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2250,14 +2253,14 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - motion-dom@12.37.0: - resolution: {integrity: sha512-LnppZuwX1jQizRWTl9LBLMN3RbAEmdQkX/2Af0UW70NCqYJI/7GfI83vQP9Ucel/Avc0Tf2ZWy8FHawuc0O6Vg==} + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} motion-utils@12.36.0: resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} - motion@12.37.0: - resolution: {integrity: sha512-Ph6oyO5hGSIAPjDsqwchEP+EKXjyFK0ci6FTIFBbx+qaMl8zLzLzPLzd9q3DKhAHcvnV7LxQonMyA+FyAv9+gA==} + motion@12.38.0: + resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2967,8 +2970,8 @@ snapshots: '@babel/generator': 7.29.1 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 @@ -2983,7 +2986,7 @@ snapshots: '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -3023,12 +3026,12 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.28.6': + '@babel/helpers@7.29.2': dependencies: '@babel/template': 7.28.6 '@babel/types': 7.29.0 - '@babel/parser@7.29.0': + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 @@ -3045,7 +3048,7 @@ snapshots: '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 '@babel/traverse@7.29.0': @@ -3053,7 +3056,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/types': 7.29.0 debug: 4.4.3 @@ -3682,7 +3685,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 '@tanstack/devtools-client': 0.0.6 @@ -3758,22 +3761,22 @@ snapshots: '@tanstack/query-core': 5.90.20 react: 19.2.4 - '@tanstack/react-router-devtools@1.166.9(@tanstack/react-router@1.167.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router-devtools@1.166.9(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/react-router': 1.167.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-devtools-core': 1.166.9(@tanstack/router-core@1.167.3)(csstype@3.2.3) + '@tanstack/react-router': 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-devtools-core': 1.166.9(@tanstack/router-core@1.167.4)(csstype@3.2.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@tanstack/router-core': 1.167.3 + '@tanstack/router-core': 1.167.4 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.167.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/history': 1.161.6 '@tanstack/react-store': 0.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-core': 1.167.3 + '@tanstack/router-core': 1.167.4 isbot: 5.1.36 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -3799,7 +3802,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/router-core@1.167.3': + '@tanstack/router-core@1.167.4': dependencies: '@tanstack/history': 1.161.6 '@tanstack/store': 0.9.2 @@ -3809,20 +3812,20 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.166.9(@tanstack/router-core@1.167.3)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.166.9(@tanstack/router-core@1.167.4)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.167.3 + '@tanstack/router-core': 1.167.4 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.166.11': + '@tanstack/router-generator@1.166.12': dependencies: - '@tanstack/router-core': 1.167.3 + '@tanstack/router-core': 1.167.4 '@tanstack/router-utils': 1.161.6 - '@tanstack/virtual-file-routes': 1.161.6 + '@tanstack/virtual-file-routes': 1.161.7 prettier: 3.8.1 recast: 0.23.11 source-map: 0.7.6 @@ -3831,7 +3834,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.166.12(@tanstack/react-router@1.167.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': + '@tanstack/router-plugin@1.166.13(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -3839,15 +3842,15 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@tanstack/router-core': 1.167.3 - '@tanstack/router-generator': 1.166.11 + '@tanstack/router-core': 1.167.4 + '@tanstack/router-generator': 1.166.12 '@tanstack/router-utils': 1.161.6 - '@tanstack/virtual-file-routes': 1.161.6 + '@tanstack/virtual-file-routes': 1.161.7 chokidar: 3.6.0 unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.167.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-router': 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -3856,7 +3859,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 ansis: 4.2.0 babel-dead-code-elimination: 1.0.12 @@ -3872,7 +3875,7 @@ snapshots: '@tanstack/virtual-core@3.13.23': {} - '@tanstack/virtual-file-routes@1.161.6': {} + '@tanstack/virtual-file-routes@1.161.7': {} '@tybys/wasm-util@0.10.1': dependencies: @@ -4006,7 +4009,7 @@ snapshots: autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001779 + caniuse-lite: 1.0.30001780 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.8 @@ -4023,7 +4026,7 @@ snapshots: babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 transitivePeerDependencies: @@ -4042,7 +4045,7 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.8 - caniuse-lite: 1.0.30001779 + caniuse-lite: 1.0.30001780 electron-to-chromium: 1.5.313 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -4069,7 +4072,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001779: {} + caniuse-lite@1.0.30001780: {} ccount@2.0.1: {} @@ -4338,9 +4341,9 @@ snapshots: fraction.js@5.3.4: {} - framer-motion@12.37.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - motion-dom: 12.37.0 + motion-dom: 12.38.0 motion-utils: 12.36.0 tslib: 2.8.1 optionalDependencies: @@ -4909,15 +4912,15 @@ snapshots: dependencies: mime-db: 1.52.0 - motion-dom@12.37.0: + motion-dom@12.38.0: dependencies: motion-utils: 12.36.0 motion-utils@12.36.0: {} - motion@12.37.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - framer-motion: 12.37.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tslib: 2.8.1 optionalDependencies: react: 19.2.4 diff --git a/web/public/assets/icons/android-icon-144x144.png b/web/public/assets/icons/android-icon-144x144.png new file mode 100644 index 0000000000..3109608b93 Binary files /dev/null and b/web/public/assets/icons/android-icon-144x144.png differ diff --git a/web/public/assets/icons/android-icon-192x192.png b/web/public/assets/icons/android-icon-192x192.png new file mode 100644 index 0000000000..a74bc05715 Binary files /dev/null and b/web/public/assets/icons/android-icon-192x192.png differ diff --git a/web/public/assets/icons/android-icon-36x36.png b/web/public/assets/icons/android-icon-36x36.png new file mode 100644 index 0000000000..90c474bf69 Binary files /dev/null and b/web/public/assets/icons/android-icon-36x36.png differ diff --git a/web/public/assets/icons/android-icon-48x48.png b/web/public/assets/icons/android-icon-48x48.png new file mode 100644 index 0000000000..6866c63414 Binary files /dev/null and b/web/public/assets/icons/android-icon-48x48.png differ diff --git a/web/public/assets/icons/android-icon-72x72.png b/web/public/assets/icons/android-icon-72x72.png new file mode 100644 index 0000000000..6469e441bd Binary files /dev/null and b/web/public/assets/icons/android-icon-72x72.png differ diff --git a/web/public/assets/icons/android-icon-96x96.png b/web/public/assets/icons/android-icon-96x96.png new file mode 100644 index 0000000000..8c9ff507c0 Binary files /dev/null and b/web/public/assets/icons/android-icon-96x96.png differ diff --git a/web/public/assets/icons/apple-icon-114x114.png b/web/public/assets/icons/apple-icon-114x114.png new file mode 100644 index 0000000000..275d37356b Binary files /dev/null and b/web/public/assets/icons/apple-icon-114x114.png differ diff --git a/web/public/assets/icons/apple-icon-120x120.png b/web/public/assets/icons/apple-icon-120x120.png new file mode 100644 index 0000000000..78b4a825f1 Binary files /dev/null and b/web/public/assets/icons/apple-icon-120x120.png differ diff --git a/web/public/assets/icons/apple-icon-144x144.png b/web/public/assets/icons/apple-icon-144x144.png new file mode 100644 index 0000000000..3109608b93 Binary files /dev/null and b/web/public/assets/icons/apple-icon-144x144.png differ diff --git a/web/public/assets/icons/apple-icon-152x152.png b/web/public/assets/icons/apple-icon-152x152.png new file mode 100644 index 0000000000..84c93350e0 Binary files /dev/null and b/web/public/assets/icons/apple-icon-152x152.png differ diff --git a/web/public/assets/icons/apple-icon-180x180.png b/web/public/assets/icons/apple-icon-180x180.png new file mode 100644 index 0000000000..3025f36c01 Binary files /dev/null and b/web/public/assets/icons/apple-icon-180x180.png differ diff --git a/web/public/assets/icons/apple-icon-57x57.png b/web/public/assets/icons/apple-icon-57x57.png new file mode 100644 index 0000000000..e0ed68dec8 Binary files /dev/null and b/web/public/assets/icons/apple-icon-57x57.png differ diff --git a/web/public/assets/icons/apple-icon-60x60.png b/web/public/assets/icons/apple-icon-60x60.png new file mode 100644 index 0000000000..99d58ac535 Binary files /dev/null and b/web/public/assets/icons/apple-icon-60x60.png differ diff --git a/web/public/assets/icons/apple-icon-72x72.png b/web/public/assets/icons/apple-icon-72x72.png new file mode 100644 index 0000000000..6469e441bd Binary files /dev/null and b/web/public/assets/icons/apple-icon-72x72.png differ diff --git a/web/public/assets/icons/apple-icon-76x76.png b/web/public/assets/icons/apple-icon-76x76.png new file mode 100644 index 0000000000..a4f840e9f4 Binary files /dev/null and b/web/public/assets/icons/apple-icon-76x76.png differ diff --git a/web/public/assets/icons/apple-icon-precomposed.png b/web/public/assets/icons/apple-icon-precomposed.png new file mode 100644 index 0000000000..a74bc05715 Binary files /dev/null and b/web/public/assets/icons/apple-icon-precomposed.png differ diff --git a/web/public/assets/icons/apple-icon.png b/web/public/assets/icons/apple-icon.png new file mode 100644 index 0000000000..a74bc05715 Binary files /dev/null and b/web/public/assets/icons/apple-icon.png differ diff --git a/web/public/assets/icons/favicon-16x16.png b/web/public/assets/icons/favicon-16x16.png new file mode 100644 index 0000000000..3874dea53d Binary files /dev/null and b/web/public/assets/icons/favicon-16x16.png differ diff --git a/web/public/assets/icons/favicon-32x32.png b/web/public/assets/icons/favicon-32x32.png new file mode 100644 index 0000000000..0b50e1f46d Binary files /dev/null and b/web/public/assets/icons/favicon-32x32.png differ diff --git a/web/public/assets/icons/favicon-96x96.png b/web/public/assets/icons/favicon-96x96.png new file mode 100644 index 0000000000..8c9ff507c0 Binary files /dev/null and b/web/public/assets/icons/favicon-96x96.png differ diff --git a/web/public/assets/icons/favicon.ico b/web/public/assets/icons/favicon.ico new file mode 100644 index 0000000000..1e0863b16c Binary files /dev/null and b/web/public/assets/icons/favicon.ico differ diff --git a/web/public/assets/icons/ms-icon-144x144.png b/web/public/assets/icons/ms-icon-144x144.png new file mode 100644 index 0000000000..3109608b93 Binary files /dev/null and b/web/public/assets/icons/ms-icon-144x144.png differ diff --git a/web/public/assets/icons/ms-icon-150x150.png b/web/public/assets/icons/ms-icon-150x150.png new file mode 100644 index 0000000000..560feeba75 Binary files /dev/null and b/web/public/assets/icons/ms-icon-150x150.png differ diff --git a/web/public/assets/icons/ms-icon-310x310.png b/web/public/assets/icons/ms-icon-310x310.png new file mode 100644 index 0000000000..90b3a1eef6 Binary files /dev/null and b/web/public/assets/icons/ms-icon-310x310.png differ diff --git a/web/public/assets/icons/ms-icon-70x70.png b/web/public/assets/icons/ms-icon-70x70.png new file mode 100644 index 0000000000..61f5c3f5a5 Binary files /dev/null and b/web/public/assets/icons/ms-icon-70x70.png differ diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx index 1854fa3af5..b6049b3bd8 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx @@ -92,6 +92,7 @@ export const SetupEdgeComponentStep = () => { )} @@ -102,6 +103,7 @@ export const SetupEdgeComponentStep = () => { )} @@ -112,6 +114,7 @@ export const SetupEdgeComponentStep = () => { )} diff --git a/web/src/pages/MigrationWizardPage/MigrationWizardPage.tsx b/web/src/pages/MigrationWizardPage/MigrationWizardPage.tsx index 30e6f66dd4..5d692eedf5 100644 --- a/web/src/pages/MigrationWizardPage/MigrationWizardPage.tsx +++ b/web/src/pages/MigrationWizardPage/MigrationWizardPage.tsx @@ -8,6 +8,7 @@ import type { } from '../../shared/components/wizard/types'; import { WizardPage } from '../../shared/components/wizard/WizardPage/WizardPage'; import { + getLocationsCountQueryOptions, getMigrationStateQueryOptions, getSettingsQueryOptions, } from '../../shared/query'; @@ -22,21 +23,26 @@ import { MigrationWizardStart } from './steps/MigrationWizardStart'; import { useMigrationWizardStore } from './store/useMigrationWizardStore'; import { MigrationWizardStep, type MigrationWizardStepValue } from './types'; -const welcomePageConfig: WizardWelcomePageConfig = { - title: 'Welcome to Defguard Migration Wizard.', - subtitle: `We've detected your previous version 1.X so email.`, - content: , - docsText: `We'll guide you through the process step by step. For full details, see the migration guide following the link below.`, -} as const; - type ConfigurableSteps = Exclude; export const MigrationWizardPage = () => { + const { data: locationCount } = useSuspenseQuery(getLocationsCountQueryOptions); const { data: wizardState } = useSuspenseQuery(getMigrationStateQueryOptions); const { data: settings } = useSuspenseQuery(getSettingsQueryOptions); const activeStep = useMigrationWizardStore((s) => s.current_step); + const welcomePageConfig = useMemo( + (): WizardWelcomePageConfig => + ({ + title: m.migration_wizard_welcome_title(), + subtitle: m.migration_wizard_welcome_subtitle({ count: locationCount }), + content: , + docsText: m.migration_wizard_welcome_docs_text(), + }) as const, + [locationCount], + ); + const stepsConfig = useMemo( (): Record => ({ general: { @@ -111,10 +117,8 @@ export const MigrationWizardPage = () => { if (settings) { useMigrationWizardStore.setState({ defguard_url: settings.defguard_url, - default_admin_group_name: settings.default_admin_group_name, - default_authentication_period_days: settings.authentication_period_days, - default_mfa_code_timeout_seconds: settings.mfa_code_timeout_seconds, public_proxy_url: settings.public_proxy_url, + ip_or_domain: settings.public_proxy_url, }); } }, [settings]); diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardCAStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardCAStep.tsx index 37b260a6b1..e6f2133fdb 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardCAStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardCAStep.tsx @@ -4,6 +4,7 @@ import z from 'zod'; import { useShallow } from 'zustand/react/shallow'; import { m } from '../../../paraglide/messages'; import api from '../../../shared/api/api'; +import type { User } from '../../../shared/api/types'; import { Controls } from '../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; @@ -14,6 +15,7 @@ import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackba import { ThemeSpacing } from '../../../shared/defguard-ui/types'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; +import { useAuth } from '../../../shared/hooks/useAuth'; import { useMigrationWizardStore } from '../store/useMigrationWizardStore'; type ValidityValue = 1 | 2 | 3 | 5 | 10; @@ -39,11 +41,12 @@ type CreateCAStoreValues = { }; export const MigrationWizardCAStep = () => { + const currentUser = useAuth((s) => s.user as User); const createCAdefaultValues = useMigrationWizardStore( useShallow( (s): CreateCAFormFields => ({ ca_common_name: s.ca_common_name, - ca_email: s.ca_email, + ca_email: s.ca_email.length ? s.ca_email : currentUser.email, ca_validity_period_years: s.ca_validity_period_years, }), ), @@ -114,6 +117,7 @@ export const MigrationWizardCAStep = () => { @@ -124,6 +128,7 @@ export const MigrationWizardCAStep = () => { )} diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardConfirmationStep/MigrationWizardConfirmationStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardConfirmationStep/MigrationWizardConfirmationStep.tsx index 9f83de1724..4071f688f7 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardConfirmationStep/MigrationWizardConfirmationStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardConfirmationStep/MigrationWizardConfirmationStep.tsx @@ -52,14 +52,14 @@ export const MigrationWizardConfirmationStep = () => { const { mutate: finish, isPending: finishPending } = useMutation({ mutationFn: migrationWizardFinishPromise, onSuccess: async () => { - Snackbar.default(`Migration finished`); + Snackbar.default(m.migration_wizard_finish_success_snackbar()); await navigate({ to: '/vpn-overview', replace: true }); setTimeout(() => { useMigrationWizardStore.getState().resetState(); }, 2500); }, onError: (e) => { - Snackbar.error(`Finishing migration failed`); + Snackbar.error(m.migration_wizard_finish_error_snackbar()); console.error(e); }, meta: { diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx index d4e7f23bb7..9f12c0270e 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx @@ -5,6 +5,7 @@ import { m } from '../../../paraglide/messages'; import { Controls } from '../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; +import { InfoBanner } from '../../../shared/defguard-ui/components/InfoBanner/InfoBanner'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; import { useAppForm } from '../../../shared/form'; @@ -66,6 +67,12 @@ export const MigrationWizardEdgeComponentStep = () => { return ( + +
{ e.stopPropagation(); @@ -79,6 +86,7 @@ export const MigrationWizardEdgeComponentStep = () => { )} @@ -89,6 +97,7 @@ export const MigrationWizardEdgeComponentStep = () => { )} @@ -99,6 +108,7 @@ export const MigrationWizardEdgeComponentStep = () => { )} diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardGeneralConfigurationStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardGeneralConfigurationStep.tsx index 31f2032a47..12863cffd3 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardGeneralConfigurationStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardGeneralConfigurationStep.tsx @@ -5,8 +5,10 @@ import { useShallow } from 'zustand/react/shallow'; import { m } from '../../../paraglide/messages'; import api from '../../../shared/api/api'; import { Controls } from '../../../shared/components/Controls/Controls'; +import { DescriptionBlock } from '../../../shared/components/DescriptionBlock/DescriptionBlock'; import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; +import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; import { useAppForm } from '../../../shared/form'; @@ -14,64 +16,40 @@ import { formChangeLogic } from '../../../shared/formLogic'; import { isValidDefguardUrl } from '../../../shared/utils/defguardUrl'; import { useMigrationWizardStore } from '../store/useMigrationWizardStore'; -type FormFields = StoreValues; - -type StoreValues = { - defguard_url: string; - default_admin_group_name: string; - default_authentication: number; - default_mfa_code_lifetime: number; - public_proxy_url: string; -}; - export const MigrationWizardGeneralConfigurationStep = () => { const { mutateAsync } = useMutation({ - mutationFn: api.migration.setGeneralConfig, + mutationFn: api.settings.patchSettings, meta: { invalidate: [['settings'], ['migration', 'state']], }, }); - const defaultValues = useMigrationWizardStore( - useShallow( - (s): FormFields => ({ - defguard_url: s.defguard_url, - default_admin_group_name: s.default_admin_group_name, - default_authentication: s.default_authentication_period_days, - default_mfa_code_lifetime: s.default_mfa_code_timeout_seconds, - public_proxy_url: s.public_proxy_url, - }), - ), - ); - const formSchema = useMemo( () => z.object({ defguard_url: z - .string({ - error: m.migration_wizard_general_config_error_defguard_url_required(), - }) - .min(1, m.migration_wizard_general_config_error_defguard_url_required()) .url(m.migration_wizard_general_config_error_invalid_url()) + .min(1, m.migration_wizard_general_config_error_defguard_url_required()) .refine( isValidDefguardUrl, m.migration_wizard_general_config_error_defguard_url_invalid_host(), ), - default_admin_group_name: z - .string() - .min(1, m.migration_wizard_general_config_error_admin_group_required()), - default_authentication: z - .number() - .min(1, m.migration_wizard_general_config_error_auth_period_min()), - default_mfa_code_lifetime: z - .number() - .min(60, m.migration_wizard_general_config_error_mfa_timeout_min()), public_proxy_url: z .url(m.migration_wizard_general_config_error_public_proxy_url_invalid()) .min(1, m.migration_wizard_general_config_error_public_proxy_url_required()), }), [], ); + type FormFields = z.infer; + + const defaultValues = useMigrationWizardStore( + useShallow( + (s): FormFields => ({ + defguard_url: s.defguard_url, + public_proxy_url: s.public_proxy_url, + }), + ), + ); const form = useAppForm({ defaultValues, @@ -84,9 +62,6 @@ export const MigrationWizardGeneralConfigurationStep = () => { await mutateAsync(value); useMigrationWizardStore.setState({ defguard_url: value.defguard_url, - default_admin_group_name: value.default_admin_group_name, - default_authentication_period_days: value.default_authentication, - default_mfa_code_timeout_seconds: value.default_mfa_code_lifetime, public_proxy_url: value.public_proxy_url, }); useMigrationWizardStore.getState().next(); @@ -103,6 +78,10 @@ export const MigrationWizardGeneralConfigurationStep = () => { }} > + +

{m.migration_wizard_general_config_private_url_description()}

+
+ {(field) => ( { /> )} - - - {(field) => ( - - )} - - - - {(field) => ( - - )} - - - - {(field) => ( - - )} - - + + +

{m.migration_wizard_general_config_public_url_description()}

+
+ {(field) => ( {
${explain2}`} + content={`${m.migration_wizard_start_explain_1()}

${m.migration_wizard_start_explain_2()}`} />