From c649eed5b8ce0548a9dc925c3b037aa5b6e21e34 Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 12 Dec 2023 17:13:16 +0100 Subject: [PATCH 01/45] feat: add interface details command (#106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add initial interface details command * update query data * fix interface name * store port and keepalive interval in stats * add new fields to interface details * update query data * update query data * add missing migration * add handler --------- Co-authored-by: Maciej Wójcik --- ...0803a080daea4ab09cc67e7a492a5b3b2d3d.json} | 6 +- ...af8a6629d1e9034aa8f863cf68bbc29dcb79d.json | 50 ------------- ...d87614e9b7c018b83107d97e48ee2713808b8.json | 62 ++++++++++++++++ ...014713157173b1c8e9f3f3ad81c10ff674744.json | 32 +++++++++ .../20231212122824_update_location_stats.sql | 2 + src-tauri/src/bin/defguard-client.rs | 10 +-- src-tauri/src/commands.rs | 71 ++++++++++++++++++- src-tauri/src/database/models/location.rs | 44 ++++++++---- src-tauri/src/utils.rs | 10 ++- 9 files changed, 212 insertions(+), 75 deletions(-) rename src-tauri/.sqlx/{query-348ae490a90a2e101e2798633813b9a65c3e71941b803d3b731fba403ebc83ab.json => query-288899ef3160db5fba8a2e91a2580803a080daea4ab09cc67e7a492a5b3b2d3d.json} (55%) delete mode 100644 src-tauri/.sqlx/query-2cac428e1501aaadd8caa5c5491af8a6629d1e9034aa8f863cf68bbc29dcb79d.json create mode 100644 src-tauri/.sqlx/query-8b1ed48665635fa226cdcbe20a5d87614e9b7c018b83107d97e48ee2713808b8.json create mode 100644 src-tauri/.sqlx/query-f0851883988e5cdf219b90562f2014713157173b1c8e9f3f3ad81c10ff674744.json create mode 100644 src-tauri/migrations/20231212122824_update_location_stats.sql diff --git a/src-tauri/.sqlx/query-348ae490a90a2e101e2798633813b9a65c3e71941b803d3b731fba403ebc83ab.json b/src-tauri/.sqlx/query-288899ef3160db5fba8a2e91a2580803a080daea4ab09cc67e7a492a5b3b2d3d.json similarity index 55% rename from src-tauri/.sqlx/query-348ae490a90a2e101e2798633813b9a65c3e71941b803d3b731fba403ebc83ab.json rename to src-tauri/.sqlx/query-288899ef3160db5fba8a2e91a2580803a080daea4ab09cc67e7a492a5b3b2d3d.json index 07196935..8ecda192 100644 --- a/src-tauri/.sqlx/query-348ae490a90a2e101e2798633813b9a65c3e71941b803d3b731fba403ebc83ab.json +++ b/src-tauri/.sqlx/query-288899ef3160db5fba8a2e91a2580803a080daea4ab09cc67e7a492a5b3b2d3d.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO location_stats (location_id, upload, download, last_handshake, collected_at) VALUES ($1, $2, $3, $4, $5) RETURNING id;", + "query": "INSERT INTO location_stats (location_id, upload, download, last_handshake, collected_at, listen_port, persistent_keepalive_interval) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id;", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 5 + "Right": 7 }, "nullable": [ false ] }, - "hash": "348ae490a90a2e101e2798633813b9a65c3e71941b803d3b731fba403ebc83ab" + "hash": "288899ef3160db5fba8a2e91a2580803a080daea4ab09cc67e7a492a5b3b2d3d" } diff --git a/src-tauri/.sqlx/query-2cac428e1501aaadd8caa5c5491af8a6629d1e9034aa8f863cf68bbc29dcb79d.json b/src-tauri/.sqlx/query-2cac428e1501aaadd8caa5c5491af8a6629d1e9034aa8f863cf68bbc29dcb79d.json deleted file mode 100644 index d8113eb9..00000000 --- a/src-tauri/.sqlx/query-2cac428e1501aaadd8caa5c5491af8a6629d1e9034aa8f863cf68bbc29dcb79d.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n WITH cte AS (\n SELECT \n id, location_id, \n COALESCE(upload - LAG(upload) OVER (PARTITION BY location_id ORDER BY collected_at), 0) as upload, \n COALESCE(download - LAG(download) OVER (PARTITION BY location_id ORDER BY collected_at), 0) as download, \n last_handshake, strftime($1, collected_at) as collected_at\n FROM location_stats\n ORDER BY collected_at\n\t LIMIT -1 OFFSET 1\n )\n SELECT \n id, location_id, \n \tSUM(MAX(upload, 0)) as \"upload!: i64\", \n \tSUM(MAX(download, 0)) as \"download!: i64\", \n \tlast_handshake, \n \tcollected_at as \"collected_at!: NaiveDateTime\"\n FROM cte\n WHERE location_id = $2\n AND collected_at >= $3\n GROUP BY collected_at\n ORDER BY collected_at;\n ", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "location_id", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "upload!: i64", - "ordinal": 2, - "type_info": "Null" - }, - { - "name": "download!: i64", - "ordinal": 3, - "type_info": "Null" - }, - { - "name": "last_handshake", - "ordinal": 4, - "type_info": "Int64" - }, - { - "name": "collected_at!: NaiveDateTime", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - false, - true, - true, - false, - true - ] - }, - "hash": "2cac428e1501aaadd8caa5c5491af8a6629d1e9034aa8f863cf68bbc29dcb79d" -} diff --git a/src-tauri/.sqlx/query-8b1ed48665635fa226cdcbe20a5d87614e9b7c018b83107d97e48ee2713808b8.json b/src-tauri/.sqlx/query-8b1ed48665635fa226cdcbe20a5d87614e9b7c018b83107d97e48ee2713808b8.json new file mode 100644 index 00000000..be4242e0 --- /dev/null +++ b/src-tauri/.sqlx/query-8b1ed48665635fa226cdcbe20a5d87614e9b7c018b83107d97e48ee2713808b8.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n WITH cte AS (\n SELECT\n id, location_id,\n COALESCE(upload - LAG(upload) OVER (PARTITION BY location_id ORDER BY collected_at), 0) as upload,\n COALESCE(download - LAG(download) OVER (PARTITION BY location_id ORDER BY collected_at), 0) as download,\n last_handshake, strftime($1, collected_at) as collected_at, listen_port, persistent_keepalive_interval\n FROM location_stats\n ORDER BY collected_at\n\t LIMIT -1 OFFSET 1\n )\n SELECT\n id, location_id,\n \tSUM(MAX(upload, 0)) as \"upload!: i64\",\n \tSUM(MAX(download, 0)) as \"download!: i64\",\n \tlast_handshake,\n \tcollected_at as \"collected_at!: NaiveDateTime\",\n \tlisten_port as \"listen_port!: u32\",\n \tpersistent_keepalive_interval as \"persistent_keepalive_interval?: u16\"\n FROM cte\n WHERE location_id = $2\n AND collected_at >= $3\n GROUP BY collected_at\n ORDER BY collected_at;\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "location_id", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "upload!: i64", + "ordinal": 2, + "type_info": "Null" + }, + { + "name": "download!: i64", + "ordinal": 3, + "type_info": "Null" + }, + { + "name": "last_handshake", + "ordinal": 4, + "type_info": "Int64" + }, + { + "name": "collected_at!: NaiveDateTime", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "listen_port!: u32", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "persistent_keepalive_interval?: u16", + "ordinal": 7, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + true, + true, + false, + true, + false, + true + ] + }, + "hash": "8b1ed48665635fa226cdcbe20a5d87614e9b7c018b83107d97e48ee2713808b8" +} diff --git a/src-tauri/.sqlx/query-f0851883988e5cdf219b90562f2014713157173b1c8e9f3f3ad81c10ff674744.json b/src-tauri/.sqlx/query-f0851883988e5cdf219b90562f2014713157173b1c8e9f3f3ad81c10ff674744.json new file mode 100644 index 00000000..97ee8817 --- /dev/null +++ b/src-tauri/.sqlx/query-f0851883988e5cdf219b90562f2014713157173b1c8e9f3f3ad81c10ff674744.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT last_handshake, listen_port as \"listen_port!: u32\",\n persistent_keepalive_interval as \"persistent_keepalive_interval?: u16\"\n FROM location_stats\n WHERE location_id = $1 ORDER BY collected_at DESC LIMIT 1\n ", + "describe": { + "columns": [ + { + "name": "last_handshake", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "listen_port!: u32", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "persistent_keepalive_interval?: u16", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "f0851883988e5cdf219b90562f2014713157173b1c8e9f3f3ad81c10ff674744" +} diff --git a/src-tauri/migrations/20231212122824_update_location_stats.sql b/src-tauri/migrations/20231212122824_update_location_stats.sql new file mode 100644 index 00000000..1adc0267 --- /dev/null +++ b/src-tauri/migrations/20231212122824_update_location_stats.sql @@ -0,0 +1,2 @@ +ALTER TABLE location_stats ADD COLUMN listen_port INTEGER NOT NULL DEFAULT 0; +ALTER TABLE location_stats ADD COLUMN persistent_keepalive_interval INTEGER NULL; diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 14ed2d8a..8c3bc075 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -12,13 +12,14 @@ use tauri_plugin_log::LogTarget; use defguard_client::{ __cmd__active_connection, __cmd__all_connections, __cmd__all_instances, __cmd__all_locations, - __cmd__connect, __cmd__disconnect, __cmd__last_connection, __cmd__location_stats, - __cmd__save_device_config, __cmd__update_instance, __cmd__update_location_routing, + __cmd__connect, __cmd__disconnect, __cmd__last_connection, __cmd__location_interface_details, + __cmd__location_stats, __cmd__save_device_config, __cmd__update_instance, + __cmd__update_location_routing, appstate::AppState, commands::{ active_connection, all_connections, all_instances, all_locations, connect, disconnect, - last_connection, location_stats, save_device_config, update_instance, - update_location_routing, + last_connection, location_interface_details, location_stats, save_device_config, + update_instance, update_location_routing, }, database, tray::create_tray_menu, @@ -79,6 +80,7 @@ async fn main() { disconnect, update_instance, location_stats, + location_interface_details, all_connections, last_connection, active_connection, diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d3621795..6fbdfa31 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -11,6 +11,7 @@ use crate::{ use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use local_ip_address::local_ip; use serde::{Deserialize, Serialize}; +use sqlx::query; use std::str::FromStr; use tauri::{AppHandle, Manager, State}; @@ -240,7 +241,6 @@ pub async fn all_instances(app_state: State<'_, AppState>) -> Result, + pub listen_port: u32, + // peer config + pub peer_pubkey: String, + pub peer_endpoint: String, + pub allowed_ips: String, + pub persistent_keepalive_interval: Option, + pub last_handshake: i64, +} + +#[tauri::command(async)] +pub async fn location_interface_details( + location_id: i64, + app_state: State<'_, AppState>, +) -> Result { + debug!("Fetching location details for location ID {location_id}"); + let pool = app_state.get_pool(); + if let Some(location) = Location::find_by_id(&pool, location_id).await? { + debug!("Fetching WireGuard keys for location {}", location.name); + let keys = WireguardKeys::find_by_instance_id(&pool, location.instance_id) + .await? + .ok_or(Error::NotFound)?; + let peer_pubkey = keys.pubkey; + + // generate interface name + #[cfg(target_os = "macos")] + let interface_name = get_interface_name(); + #[cfg(not(target_os = "macos"))] + let interface_name = get_interface_name(&location); + + let result = query!( + r#" + SELECT last_handshake, listen_port as "listen_port!: u32", + persistent_keepalive_interval as "persistent_keepalive_interval?: u16" + FROM location_stats + WHERE location_id = $1 ORDER BY collected_at DESC LIMIT 1 + "#, + location_id + ) + .fetch_one(&pool) + .await?; + + Ok(LocationInterfaceDetails { + location_id, + name: interface_name, + pubkey: location.pubkey, + address: location.address, + dns: location.dns, + listen_port: result.listen_port, + peer_pubkey, + peer_endpoint: location.endpoint, + allowed_ips: location.allowed_ips, + persistent_keepalive_interval: result.persistent_keepalive_interval, + last_handshake: result.last_handshake, + }) + } else { + error!("Location ID {location_id} not found"); + Err(Error::NotFound) + } +} + #[tauri::command(async)] pub async fn update_instance( instance_id: i64, diff --git a/src-tauri/src/database/models/location.rs b/src-tauri/src/database/models/location.rs index 6c7b286f..0e8ae840 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -29,9 +29,15 @@ pub struct LocationStats { download: i64, last_handshake: i64, collected_at: NaiveDateTime, + listen_port: u32, + persistent_keepalive_interval: Option, } -pub async fn peer_to_location_stats(peer: &Peer, pool: &DbPool) -> Result { +pub async fn peer_to_location_stats( + peer: &Peer, + listen_port: u32, + pool: &DbPool, +) -> Result { let location = Location::find_by_public_key(pool, &peer.public_key.to_string()).await?; Ok(LocationStats { id: None, @@ -43,6 +49,8 @@ pub async fn peer_to_location_stats(peer: &Peer, pool: &DbPool) -> Result, ) -> Self { LocationStats { id: None, @@ -204,19 +214,23 @@ impl LocationStats { download, last_handshake, collected_at, + listen_port, + persistent_keepalive_interval, } } pub async fn save(&mut self, pool: &DbPool) -> Result<(), Error> { let result = query!( - "INSERT INTO location_stats (location_id, upload, download, last_handshake, collected_at) \ - VALUES ($1, $2, $3, $4, $5) \ + "INSERT INTO location_stats (location_id, upload, download, last_handshake, collected_at, listen_port, persistent_keepalive_interval) \ + VALUES ($1, $2, $3, $4, $5, $6, $7) \ RETURNING id;", self.location_id, self.upload, self.download, self.last_handshake, self.collected_at, + self.listen_port, + self.persistent_keepalive_interval, ) .fetch_one(pool) .await?; @@ -235,21 +249,23 @@ impl LocationStats { LocationStats, r#" WITH cte AS ( - SELECT - id, location_id, - COALESCE(upload - LAG(upload) OVER (PARTITION BY location_id ORDER BY collected_at), 0) as upload, - COALESCE(download - LAG(download) OVER (PARTITION BY location_id ORDER BY collected_at), 0) as download, - last_handshake, strftime($1, collected_at) as collected_at + SELECT + id, location_id, + COALESCE(upload - LAG(upload) OVER (PARTITION BY location_id ORDER BY collected_at), 0) as upload, + COALESCE(download - LAG(download) OVER (PARTITION BY location_id ORDER BY collected_at), 0) as download, + last_handshake, strftime($1, collected_at) as collected_at, listen_port, persistent_keepalive_interval FROM location_stats ORDER BY collected_at LIMIT -1 OFFSET 1 ) - SELECT - id, location_id, - SUM(MAX(upload, 0)) as "upload!: i64", - SUM(MAX(download, 0)) as "download!: i64", - last_handshake, - collected_at as "collected_at!: NaiveDateTime" + SELECT + id, location_id, + SUM(MAX(upload, 0)) as "upload!: i64", + SUM(MAX(download, 0)) as "download!: i64", + last_handshake, + collected_at as "collected_at!: NaiveDateTime", + listen_port as "listen_port!: u32", + persistent_keepalive_interval as "persistent_keepalive_interval?: u16" FROM cte WHERE location_id = $2 AND collected_at >= $3 diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 8146f6e0..05b2d136 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -178,9 +178,13 @@ pub async fn spawn_stats_thread(handle: tauri::AppHandle, interface_name: String let peers: Vec = interface_data.peers.into_iter().map(Into::into).collect(); for peer in peers { - let mut location_stats = peer_to_location_stats(&peer, &state.get_pool()) - .await - .unwrap(); + let mut location_stats = peer_to_location_stats( + &peer, + interface_data.listen_port, + &state.get_pool(), + ) + .await + .unwrap(); debug!("Saving location stats: {location_stats:#?}"); let _ = location_stats.save(&state.get_pool()).await; debug!("Saved location stats: {location_stats:#?}"); From 5360ffe96ca5c23dd7089dfae1a2a264807d78d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:57:43 +0100 Subject: [PATCH 02/45] feat: settings page (#109) --- ...38752436bad31f3e78ed9efd5f2892c779405.json | 12 + ...f73380dc75c43d1c27e7d4ea47cd6adb6273a.json | 12 + ...d217b7e37fb90d7d343637bf7d00845fcfdf2.json | 38 +++ src-tauri/Cargo.lock | 57 ++++- src-tauri/Cargo.toml | 4 +- .../20231212110739_add_settings.sql | 6 + .../resources/icons/tray-32x32-black.png | Bin 0 -> 2088 bytes .../resources/icons/tray-32x32-color.png | Bin 0 -> 1704 bytes src-tauri/resources/icons/tray-32x32-gray.png | Bin 0 -> 2090 bytes .../resources/icons/tray-32x32-white.png | Bin 0 -> 2078 bytes src-tauri/src/bin/defguard-client.rs | 20 +- src-tauri/src/commands.rs | 25 +- src-tauri/src/database/mod.rs | 2 + src-tauri/src/database/models/mod.rs | 1 + src-tauri/src/database/models/settings.rs | 102 ++++++++ src-tauri/src/error.rs | 4 + src-tauri/src/tray.rs | 24 +- src-tauri/tauri.conf.json | 7 +- src/components/App/App.tsx | 40 +++- src/i18n/en/index.ts | 34 +++ src/i18n/i18n-types.ts | 176 +++++++++++++- src/pages/client/clientAPI/clientApi.ts | 9 +- src/pages/client/clientAPI/types.ts | 15 +- .../ClientSideBar/ClientSideBar.tsx | 25 +- .../components/ClientSideBar/style.scss | 11 +- src/pages/client/hooks/useClientStore.tsx | 14 +- .../ClientSettingsPage/ClientSettingsPage.tsx | 20 ++ .../GlobalSettingsTab/GlobalSettingsTab.tsx | 221 ++++++++++++++++++ .../components/GlobalSettingsTab/style.scss | 21 ++ .../pages/ClientSettingsPage/style.scss | 27 +++ src/shared/defguard-ui | 2 +- .../providers/ThemeProvider/ThemeProvider.tsx | 21 ++ src/shared/routes.ts | 1 + 33 files changed, 924 insertions(+), 27 deletions(-) create mode 100644 src-tauri/.sqlx/query-72a50e18c40e37340c86bdbdea938752436bad31f3e78ed9efd5f2892c779405.json create mode 100644 src-tauri/.sqlx/query-99c5e0df821b871da73371eb5edf73380dc75c43d1c27e7d4ea47cd6adb6273a.json create mode 100644 src-tauri/.sqlx/query-d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2.json create mode 100644 src-tauri/migrations/20231212110739_add_settings.sql create mode 100644 src-tauri/resources/icons/tray-32x32-black.png create mode 100644 src-tauri/resources/icons/tray-32x32-color.png create mode 100644 src-tauri/resources/icons/tray-32x32-gray.png create mode 100644 src-tauri/resources/icons/tray-32x32-white.png create mode 100644 src-tauri/src/database/models/settings.rs create mode 100644 src/pages/client/pages/ClientSettingsPage/ClientSettingsPage.tsx create mode 100644 src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx create mode 100644 src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/style.scss create mode 100644 src/pages/client/pages/ClientSettingsPage/style.scss create mode 100644 src/shared/providers/ThemeProvider/ThemeProvider.tsx diff --git a/src-tauri/.sqlx/query-72a50e18c40e37340c86bdbdea938752436bad31f3e78ed9efd5f2892c779405.json b/src-tauri/.sqlx/query-72a50e18c40e37340c86bdbdea938752436bad31f3e78ed9efd5f2892c779405.json new file mode 100644 index 00000000..36cb4aa3 --- /dev/null +++ b/src-tauri/.sqlx/query-72a50e18c40e37340c86bdbdea938752436bad31f3e78ed9efd5f2892c779405.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE settings SET theme = $1, log_level = $2, tray_icon_theme = $3 WHERE id = 1;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "72a50e18c40e37340c86bdbdea938752436bad31f3e78ed9efd5f2892c779405" +} diff --git a/src-tauri/.sqlx/query-99c5e0df821b871da73371eb5edf73380dc75c43d1c27e7d4ea47cd6adb6273a.json b/src-tauri/.sqlx/query-99c5e0df821b871da73371eb5edf73380dc75c43d1c27e7d4ea47cd6adb6273a.json new file mode 100644 index 00000000..71eacea0 --- /dev/null +++ b/src-tauri/.sqlx/query-99c5e0df821b871da73371eb5edf73380dc75c43d1c27e7d4ea47cd6adb6273a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO settings (log_level, theme, tray_icon_theme) VALUES ($1, $2, $3);", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "99c5e0df821b871da73371eb5edf73380dc75c43d1c27e7d4ea47cd6adb6273a" +} diff --git a/src-tauri/.sqlx/query-d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2.json b/src-tauri/.sqlx/query-d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2.json new file mode 100644 index 00000000..ed4837cc --- /dev/null +++ b/src-tauri/.sqlx/query-d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM settings WHERE id = 1;", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "theme", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "log_level", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "tray_icon_theme", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2" +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3985ea72..6a86f292 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1044,6 +1044,8 @@ dependencies = [ "serde", "serde_json", "sqlx", + "struct-patch", + "strum", "tauri", "tauri-build", "tauri-plugin-log", @@ -2201,6 +2203,15 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a" +dependencies = [ + "cfb", +] + [[package]] name = "infer" version = "0.13.0" @@ -4464,6 +4475,48 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "struct-patch" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c52ef523e89b3172242bbabefd8a92493ae5571224c29ed2f00185c39b395c2" +dependencies = [ + "struct-patch-derive", +] + +[[package]] +name = "struct-patch-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14a349c27ebe59faba22f933c9c734d428da7231e88a247e9d8c61eea964ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.39", +] + [[package]] name = "subtle" version = "2.5.0" @@ -4642,9 +4695,11 @@ dependencies = [ "heck 0.4.1", "http", "ignore", + "infer 0.9.0", "objc", "once_cell", "percent-encoding", + "png", "rand 0.8.5", "raw-window-handle", "reqwest", @@ -4809,7 +4864,7 @@ dependencies = [ "glob", "heck 0.4.1", "html5ever 0.26.0", - "infer", + "infer 0.13.0", "json-patch", "kuchikiki", "log", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d40b5956..6890c113 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,14 +34,16 @@ x25519-dalek = { version = "2", features = [ "getrandom", "static_secrets", ] } +strum = { version = "0.25", features = ["derive"] } -tauri = { version = "1.5", features = [ "http-all", "window-all", "system-tray", "native-tls-vendored"] } +tauri = { version = "1.5", features = [ "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } lazy_static = "1.4" +struct-patch = "0.4" [target.'cfg(target_os = "macos")'.dependencies] nix = { version = "0.27", features = ["net"] } diff --git a/src-tauri/migrations/20231212110739_add_settings.sql b/src-tauri/migrations/20231212110739_add_settings.sql new file mode 100644 index 00000000..f75c381c --- /dev/null +++ b/src-tauri/migrations/20231212110739_add_settings.sql @@ -0,0 +1,6 @@ +CREATE TABLE settings( + id INTEGER PRIMARY KEY AUTOINCREMENT, + theme TEXT DEFAULT 'light' NOT NULL, + log_level TEXT DEFAULT 'info' NOT NULL, + tray_icon_theme TEXT DEFAULT 'color' NOT NULL +); \ No newline at end of file diff --git a/src-tauri/resources/icons/tray-32x32-black.png b/src-tauri/resources/icons/tray-32x32-black.png new file mode 100644 index 0000000000000000000000000000000000000000..e0f7e171e9ce1775c6dd57cf250aa0c49a65b0df GIT binary patch literal 2088 zcmbVNeM}Q)9ByHX$cN796gA^=s1e8YKH4ko*@8-$d=(J^5kJQD?k&C1_Kw~`3vLTe z7SNeF7a~ebWIAWED9Ar%PKQPi2e@fAw?#JTMiFf)J(BiY`?Jky3jB zk|Fv4V%CuuM-U{E3RearWip#n5Q?DmIAO#o8lwn?AQ_rQLO&GB`FJN&XnQRr2CuBB zOHm{S$Nhf4∾rz6zW)o6R^u;S_~I1SZ#d6*ho*<)m>28<07lASr_AMN~%CAyz9^ z6j~jN!6Sufy>ci|Fk^Utm2gr=s4)eBJQv1E)joGnoab=h1|Hy5WQZlhSjitQU_bEFmkIH86}&LLQ5ji)tl+7Xn$V_HiJy2HH$gqmh^lA7B;H zS0sw=@k$kpTSh2@E)`k0TJZ9sUrv5#1F*3Qu%fVP6h@L5VJ;$RhB7e*)2py{1ThA+ zi@e~h4MX)93DNo@l4A4}LmM7}!k*z-g?%QN=NPBx^RO^r!NXPnT=G_+NH~p5hUgZ3 zP#C(?2VL0hOtx26ST6^%ZB`VsFLgiwt-)bX#~#KAuZCwBPRnkn}0VA5)h_r{ZUQU}*)+-7}jD zf0B8bt$Dij=PTuyr?B-WHRJ!W9!d-6V!agrPEH(EgTd7iGgcJ*zx4_}duSYu!{Llm zm*MFOFT(Q~cpacg51?D+Vjf|LvM=wr>f%_)TLQd6wHap1b!7KQ!gVgkCA;!Ml-dkq3wAiKCme zhk9qP>&}WOnX4%ycg5b1-AG`4!s!hbJhD9PYEN5B=S_SuU0_(UG2%k;nW{U@(MJyF zBz+pU#9h0*-IJ8oSkkXO75hhT?uk?Rc`NeXu;!O+OxzY#9<}qvmZ^~I&m$-UBH}GRs#9rTb!&T>Whf~fkz1WzvVS8zdC9f_w>dWG8 zd-TPA4a`9NCogqOv#wjlVcOY4U;O>TL89Yw*GX;Twtds~ewb9Me#^5n^KI?R%HR11 D1X0YD literal 0 HcmV?d00001 diff --git a/src-tauri/resources/icons/tray-32x32-color.png b/src-tauri/resources/icons/tray-32x32-color.png new file mode 100644 index 0000000000000000000000000000000000000000..8cc5ae211119f01d7538b7b900ec13a97ff6a547 GIT binary patch literal 1704 zcmV;Z23PrsP)9>iYfIIg!9xPI5>cpd=EDy#SEg=JX|%AB`2?qq$VZhS^M zKnOkizV84a$3b2Y08$iBNlfn{FN!@+NbP2`VA2L?6H_L7nge|YKv$tfFhW?7W$mcc z@i7Ogu10-Uan?WnrIA>6yV+wD9NDsB2SBmgY<(>&cvYJ$N{eDel7xsTQ|OYd+k#%1 zzW|D=3}U{hzSVVlf63!Lt5zwUZEYA~h>?nVAOJvG910?5%vxCxoV9WdA7Tuo^vmWh z8{m0?57PS^<}AwzNO^U(<>!NYkXw=|076cm0Q#bUE`W7uvtN(;^5&h;rCYW^y)h6u z0kEDcE4%;R*ip^PQj=|0&YnXq0Qf=Crr?F^Hv_+bAVdklUTFEb?=UU_HqKkS+O*@u zDZvyMk0bz`wSU$;D9D`Fx^Cu7sinTj-SGGA|MD`XwieEL!_`<cXoW~gv05DFk`;L^*~z2r*M2#}tVKPp{^f1wkDqK_G-_DKp`y*0ynrYGgt5;6 zkjv@B#Km$)TWif{#M~_SfR#z~YA!qG=S9(AndSotDho3NE9<5v?DZ%Ii76> z+dJA-0Ep7(%QW=R$N6ih$bi=Pf?vi5Ynkn6LFmbO$UzkZv4N=Z6Q}2I!h+U-B-z*V zfC?U&?=@m__``x;D`tLJq+L-_$0ugapp%IWB{r5Z5|ZDXr_DcKFHX+M$C9MbE_fqC zz8Cc&I(*RGYnM?|)m1oX#2BEDhyc3KFh1$EEG}wH3VLv>nhS}J^?Q5o2_bDsF3=ba z5~JLByoY1V)^7Y5z|5u8d*IrGxvBqZ?o` z(4SBvpzcmds@%1myq6$fIaGp)Es%`7I+1wwy+9qO>?@dnZI8|J4{P3s)kpU6)7I_8 zlD!+17v*AeBeNvbg@quR7UY1$vSUC1^w5E*`ibHRr})9G8u{>Blb`ld5MV^YNH{rf z8xgIJMlley`T+#+j5w*uN#HPH(M+Hn3Ku@AA9_b|zNJuE^%!Me7ctReDR-AP*q{^Wp)rJ6^xe(c$s6``l`60-wQphPu z{((xB4p_!ngU*D%GEWQ&Qdr4;U<0)ADzp-)YAnexByDjrMvgUe2J;kDJDTo?IwV1K z*Cn8Ol0j&_lVLf%o->#pf})-gc$I%DSOA<`3I%u+uo&PykdlKQ0#Bro%a{C82ni#1 z`j`ucgR=(}l@9`Fw^<1kvrZHR&ZO5fxdsSG-U0xrHyKQ%i-snW%|%PVSb&*L?6*mv zR;#SGzrV``2_TMtRZY-s04~(LZi5Aqxq1Un@=(YnO=f{M33`K@H5&)q%n6C;MdSSs zvTCjb#AsxA7V?6NWPs63Lc!oBUAYE<)Ej}D=K;^SUA)%Ku?FD^MFo|vF8+R0z_;To zzlg_rg5!bK>sA72MTPFa3gMWa7?h}|~BS#|iApNdv|P?p(olXrKsfNowF+BV_E>W|LtTC?V2 ziVu3-n+q$){JC-a_VcZ6HEH&&lq1QG^3_GVZO4D};ic@(*-3fU zPr3=+#?jrrUU<4*I^u~&41u+gnU!>gO7+l0&`~UUo&?{+yd#9 z-C2#JTf4B+jf;h-=X`Uvy+nUIS~nI`ZcZA!Z{?tOFKp}El*FYqw7k0N%dN!?U(L-b z3t+Aj89kl3QN_Du99Dc-x3@j@+QD<}n}56{wB0#Wg?$UgwnXP`x_fElmD%Se?>|#& zIJlubsX(S&hR#NL%#Mz(7LfFLd%?KjR|6f)JNEq}Y5+9-fMtF^zqlav_VOEN*B3x< z4PE)!vcbWqdtAZgU}jFQ>DtlypHDX3@VsI3weC>I=S>-IyXU)>U*y;@vb#0&{XXNA zzWdnOt9NsH{+@I4|BY?aA9km*?acYF~5v%o1D6Yc)&$0pJAQ Ai2wiq literal 0 HcmV?d00001 diff --git a/src-tauri/resources/icons/tray-32x32-white.png b/src-tauri/resources/icons/tray-32x32-white.png new file mode 100644 index 0000000000000000000000000000000000000000..e5ede2fada84e0a5629b644aa054ed8d73a54803 GIT binary patch literal 2078 zcmbVNeNYr-99|$LQASiC1yuB(o_0uy>E+(#EgpX7={o-RFIt z=l8L{T~|^(D?TnI4ndIk!UD@Yct+LZ(b4cwaaJxJ*8!%xZUKXy^+XRM&AWEePo2d$-c54Z0 zmV5wZG!)L0B!wE_N|!|&8RjXJCTT4}W)ZX=r%9HiSiK$%|1c=$6CCV3OMX}kUYRhb zqIg+?@caE5zg8ppDhP^U7=ok;n#Lglmuox<7r;Go`Urys$h=SVDx%~;RYtB{s#Z)G zv^pGv+Z&5P-S0S|b{F_2PC%BxEIT1q>sckh`K~Ta6bG^MEW>`*@JI0@_Shqw%t4AK(

yUhI4hAf`xe7pdJrx)lNh52PT#^q8 zLwDMc3#*ka^vDY5;X$Frgu$3KqA0)?%gSOXPK!HeN{8zNogOczjU+A@7)O?#6KEZ9 zjO@2ae6?Cxb^mae3la}G{#6Bn!Jz}Z9_IzE0nc*iNZeS?aJa$9IrKcIqZl&6HpeHz z7mah>&Z@c+AS0tCjanVe;JlF`aRbl++(_!maji~E!bf@ls4TUcLk+?fh%zi)P5Awo z530f|mxzXXg5`L%*G(9&78wXw__lbLd_0tLc)#BXAnARIKddfG4#m&;z|0Dmy9YKG zaWC^Sx8nZRAFh-Q0{sj39#~bBw^CDo)ZVb9Yt7l0%Mwl;4aT%3&A67?T$g9q#4(z8 z&JN6-{LG}<=C3#IikZ~R#P_Y6!61(FVEVrL&+2|`|8-l`;)(TE{q(Dc$8_w|BHvO6lwQqPf&sUKAF1rt`SU*jCLJ9s{3Ltkn?(ed=My!K*a^mS_B`OXWs z#-paDIou!9e(KqOd0W(*RfhJVm3{S9U2hNeHO4Y2&2h=+#^ucVjo5YUrL8-i-(QU0 zURI==zOT!?tYL@s^3>Aq)0_63%Q!Rsa?{4L#XY;T7c4bi2=;bgJoIkoxcZM%Jq2x> z9w`x#_LJAL+Z(PAR=18WnRaXJ_^LE*<-R{-k2iH=!Is4i^$T8@S8Un&Y}x9608cK} ANB{r; literal 0 HcmV?d00001 diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 8c3bc075..c7e993c4 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -12,17 +12,17 @@ use tauri_plugin_log::LogTarget; use defguard_client::{ __cmd__active_connection, __cmd__all_connections, __cmd__all_instances, __cmd__all_locations, - __cmd__connect, __cmd__disconnect, __cmd__last_connection, __cmd__location_interface_details, - __cmd__location_stats, __cmd__save_device_config, __cmd__update_instance, - __cmd__update_location_routing, + __cmd__connect, __cmd__disconnect, __cmd__get_settings, __cmd__last_connection, + __cmd__location_interface_details, __cmd__location_stats, __cmd__save_device_config, + __cmd__update_instance, __cmd__update_location_routing, __cmd__update_settings, appstate::AppState, commands::{ active_connection, all_connections, all_instances, all_locations, connect, disconnect, - last_connection, location_interface_details, location_stats, save_device_config, - update_instance, update_location_routing, + get_settings, last_connection, location_interface_details, location_stats, + save_device_config, update_instance, update_location_routing, update_settings, }, - database, - tray::create_tray_menu, + database::{self, models::settings::Settings}, + tray::{configure_tray_icon, create_tray_menu}, utils::load_log_targets, }; use std::{env, str::FromStr}; @@ -85,6 +85,8 @@ async fn main() { last_connection, active_connection, update_location_routing, + get_settings, + update_settings, ]) .on_window_event(|event| match event.event() { tauri::WindowEvent::CloseRequested { api, .. } => { @@ -174,6 +176,10 @@ async fn main() { info!("Starting main app thread."); let result = database::info(&app_state.get_pool()).await; info!("Database info result: {:#?}", result); + // configure tray + if let Ok(settings) = Settings::get(&app_state.get_pool()).await { + configure_tray_icon(&handle, &settings.tray_icon_theme).unwrap(); + } }); Ok(()) }) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 6fbdfa31..4771144f 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,11 +1,13 @@ use crate::{ appstate::AppState, database::{ - models::instance::InstanceInfo, ActiveConnection, Connection, ConnectionInfo, Instance, - Location, LocationStats, WireguardKeys, + models::{instance::InstanceInfo, settings::SettingsPatch}, + ActiveConnection, Connection, ConnectionInfo, Instance, Location, LocationStats, Settings, + WireguardKeys, }, error::Error, service::proto::RemoveInterfaceRequest, + tray::configure_tray_icon, utils::{get_interface_name, setup_interface, spawn_stats_thread}, }; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; @@ -13,6 +15,7 @@ use local_ip_address::local_ip; use serde::{Deserialize, Serialize}; use sqlx::query; use std::str::FromStr; +use struct_patch::Patch; use tauri::{AppHandle, Manager, State}; #[derive(Clone, serde::Serialize)] @@ -511,3 +514,21 @@ pub async fn update_location_routing( Err(Error::NotFound) } } + +#[tauri::command] +pub async fn get_settings(handle: AppHandle) -> Result { + let app_state = handle.state::(); + let settings = Settings::get(&app_state.get_pool()).await?; + Ok(settings) +} + +#[tauri::command] +pub async fn update_settings(data: SettingsPatch, handle: AppHandle) -> Result { + let app_state = handle.state::(); + let pool = &app_state.get_pool(); + let mut settings = Settings::get(pool).await?; + settings.apply(data); + settings.save(pool).await?; + configure_tray_icon(&handle, &settings.tray_icon_theme)?; + Ok(settings) +} diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 05cb02da..d1af75c2 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -41,6 +41,7 @@ pub async fn init_db(app_handle: &AppHandle) -> Result { let pool = DbPool::connect(&format!("sqlite://{}", db_path.to_str().unwrap())).await?; debug!("Running migrations."); sqlx::migrate!().run(&pool).await?; + Settings::init_defaults(&pool).await?; info!("Applied migrations."); Ok(pool) } @@ -62,5 +63,6 @@ pub use models::{ connection::{ActiveConnection, Connection, ConnectionInfo}, instance::{Instance, InstanceInfo}, location::{Location, LocationStats}, + settings::{Settings, SettingsLogLevel, SettingsTheme, TrayIconTheme}, wireguard_keys::WireguardKeys, }; diff --git a/src-tauri/src/database/models/mod.rs b/src-tauri/src/database/models/mod.rs index 0fc7d2b8..a73cb553 100644 --- a/src-tauri/src/database/models/mod.rs +++ b/src-tauri/src/database/models/mod.rs @@ -1,4 +1,5 @@ pub mod connection; pub mod instance; pub mod location; +pub mod settings; pub mod wireguard_keys; diff --git a/src-tauri/src/database/models/settings.rs b/src-tauri/src/database/models/settings.rs new file mode 100644 index 00000000..4528bdab --- /dev/null +++ b/src-tauri/src/database/models/settings.rs @@ -0,0 +1,102 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use sqlx::{query, FromRow, Type}; +use struct_patch::Patch; +use strum::{AsRefStr, EnumString}; + +use crate::{database::DbPool, error::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Type, EnumString)] +#[sqlx(type_name = "theme", rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum SettingsTheme { + Light, + Dark, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Type, EnumString)] +#[sqlx(type_name = "log_level", rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum SettingsLogLevel { + Error, + Info, + Debug, + Trace, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Type, EnumString, AsRefStr)] +#[sqlx(type_name = "tray_icon_theme", rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum TrayIconTheme { + Color, + White, + Black, + Gray, +} + +#[derive(FromRow, Debug, Serialize, Deserialize, Patch)] +#[patch_derive(Debug, Serialize, Deserialize)] +pub struct Settings { + #[serde(skip)] + pub id: Option, + pub theme: SettingsTheme, + pub log_level: SettingsLogLevel, + pub tray_icon_theme: TrayIconTheme, +} + +impl Settings { + pub async fn get(pool: &DbPool) -> Result { + let query_res = query!("SELECT * FROM settings WHERE id = 1;") + .fetch_one(pool) + .await?; + let settings = Self { + id: Some(query_res.id), + log_level: SettingsLogLevel::from_str(&query_res.log_level)?, + theme: SettingsTheme::from_str(&query_res.theme)?, + tray_icon_theme: TrayIconTheme::from_str(&query_res.tray_icon_theme)?, + }; + Ok(settings) + } + + pub async fn save(&mut self, pool: &DbPool) -> Result<(), Error> { + query!( + "UPDATE settings \ + SET theme = $1, log_level = $2, tray_icon_theme = $3 \ + WHERE id = 1;", + self.theme, + self.log_level, + self.tray_icon_theme + ) + .execute(pool) + .await?; + Ok(()) + } + + // checks if settings is empty and insert default settings if they not exist, this should be called before app start + pub async fn init_defaults(pool: &DbPool) -> Result<(), Error> { + let current_config = query!("SELECT * FROM settings WHERE id = 1;") + .fetch_optional(pool) + .await?; + if current_config.is_none() { + let default_settings = Settings { + id: None, + log_level: SettingsLogLevel::Info, + theme: SettingsTheme::Light, + tray_icon_theme: TrayIconTheme::Color, + }; + query!( + "INSERT INTO settings (log_level, theme, tray_icon_theme) VALUES ($1, $2, $3);", + default_settings.log_level, + default_settings.theme, + default_settings.tray_icon_theme, + ) + .execute(pool) + .await?; + } + Ok(()) + } +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index b3cbb54c..1c531e96 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -34,6 +34,10 @@ pub enum Error { NotFound, #[error("Tauri error: {0}")] Tauri(#[from] tauri::Error), + #[error("Failed to parse str to enum")] + StrumError(#[from] strum::ParseError), + #[error("Required resource not found {0}")] + ResourceNotFound(String), } // we must manually implement serde::Serialize diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 682b1b04..473c70a3 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -1,4 +1,6 @@ -use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem}; +use tauri::{AppHandle, CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem}; + +use crate::{database::TrayIconTheme, error::Error}; #[must_use] pub fn create_tray_menu() -> SystemTrayMenu { @@ -11,3 +13,23 @@ pub fn create_tray_menu() -> SystemTrayMenu { .add_native_item(SystemTrayMenuItem::Separator) .add_item(quit) } + +pub fn configure_tray_icon(app: &AppHandle, theme: &TrayIconTheme) -> Result<(), Error> { + let resource_str = format!("resources/icons/tray-32x32-{}.png", theme.as_ref()); + debug!("Tray icon loading from {:?}", &resource_str); + match app.path_resolver().resolve_resource(&resource_str) { + Some(icon_path) => { + let icon = tauri::Icon::File(icon_path); + app.tray_handle().set_icon(icon)?; + debug!("Tray icon changed"); + Ok(()) + } + None => { + error!( + "Loading tray icon resource {} failed! Resource not resolved.", + &resource_str + ); + Err(Error::ResourceNotFound(resource_str)) + } + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 77130108..1d4e5121 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,10 +12,9 @@ }, "tauri": { "systemTray": { - "iconPath": "icons/32x32.png", + "iconPath": "resources/icons/tray-32x32-color.png", "iconAsTemplate": false }, - "allowlist": { "all": false, "window": { @@ -58,7 +57,9 @@ "providerShortName": null, "signingIdentity": null }, - "resources": [], + "resources": [ + "resources/*" + ], "shortDescription": "", "targets": ["deb", "app", "appimage"], "windows": { diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index e7ce4522..506bfb7b 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -12,7 +12,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import timezone from 'dayjs/plugin/timezone'; import updateLocale from 'dayjs/plugin/updateLocale'; import utc from 'dayjs/plugin/utc'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; import { debug } from 'tauri-plugin-log-api'; import { localStorageDetector } from 'typesafe-i18n/detectors'; @@ -20,12 +20,16 @@ import { localStorageDetector } from 'typesafe-i18n/detectors'; import TypesafeI18n from '../../i18n/i18n-react'; import { detectLocale } from '../../i18n/i18n-util'; import { loadLocaleAsync } from '../../i18n/i18n-util.async'; +import { clientApi } from '../../pages/client/clientAPI/clientApi'; import { ClientPage } from '../../pages/client/ClientPage'; +import { useClientStore } from '../../pages/client/hooks/useClientStore'; import { ClientAddInstancePage } from '../../pages/client/pages/ClientAddInstancePage/ClientAddInstnacePage'; import { ClientInstancePage } from '../../pages/client/pages/ClientInstancePage/ClientInstancePage'; +import { ClientSettingsPage } from '../../pages/client/pages/ClientSettingsPage/ClientSettingsPage'; import { EnrollmentPage } from '../../pages/enrollment/EnrollmentPage'; import { SessionTimeoutPage } from '../../pages/sessionTimeout/SessionTimeoutPage'; import { ToastManager } from '../../shared/defguard-ui/components/Layout/ToastManager/ToastManager'; +import { ThemeProvider } from '../../shared/providers/ThemeProvider/ThemeProvider'; import { routes } from '../../shared/routes'; dayjs.extend(duration); @@ -38,6 +42,8 @@ dayjs.extend(timezone); const queryClient = new QueryClient(); +const { getSettings } = clientApi; + const router = createBrowserRouter([ { index: true, @@ -64,6 +70,10 @@ const router = createBrowserRouter([ path: '/client/add-instance', element: , }, + { + path: '/client/settings', + element: , + }, { path: '/client/*', element: , @@ -79,8 +89,16 @@ const router = createBrowserRouter([ const detectedLocale = detectLocale(localStorageDetector); export const App = () => { - const [wasLoaded, setWasLoaded] = useState(false); + const [localeLoaded, setWasLoaded] = useState(false); + const [settingsLoaded, setSettingsLoaded] = useState(false); + const setClientState = useClientStore((state) => state.setState); + const appLoaded = useMemo( + () => localeLoaded && settingsLoaded, + [localeLoaded, settingsLoaded], + ); + + // load locales useEffect(() => { debug('Loading locales'); loadLocaleAsync(detectedLocale).then(() => { @@ -90,12 +108,26 @@ export const App = () => { dayjs.locale(detectedLocale); }, []); - if (!wasLoaded) return null; + // load settings from tauri first time + useEffect(() => { + const loadSettings = async () => { + debug('Loading settings from tauri'); + const res = await getSettings(); + setClientState({ settings: res }); + debug('Settings loaded and set'); + setSettingsLoaded(true); + }; + loadSettings(); + }, [setClientState, setSettingsLoaded]); + + if (!appLoaded) return null; return ( - + + + diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index dd8cb6ed..50915306 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -52,6 +52,39 @@ const en = { pages: { client: { pages: { + settingsPage: { + title: 'Settings', + tabs: { + global: { + tray: { + title: 'System tray', + label: 'Tray icon theme', + options: { + color: 'Color', + white: 'White', + black: 'Black', + gray: 'Gray', + }, + }, + logging: { + title: 'Logging threshold', + options: { + error: 'Error', + info: 'Info', + debug: 'Debug', + trace: 'Trace', + }, + }, + theme: { + title: 'Theme', + options: { + light: 'Light', + dark: 'Dark', + }, + }, + }, + }, + }, instancePage: { title: 'Locations', controls: { @@ -160,6 +193,7 @@ const en = { sideBar: { instances: 'Instances', addInstance: 'Add Instance', + settings: 'Settings', copyright: { copyright: `Copyright © 2023`, appVersion: 'Application version: {version:string}', diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 8d83bdcc..c5feae5d 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -136,6 +136,84 @@ type RootTranslation = { pages: { client: { pages: { + settingsPage: { + /** + * S​e​t​t​i​n​g​s + */ + title: string + tabs: { + global: { + tray: { + /** + * S​y​s​t​e​m​ ​t​r​a​y + */ + title: string + /** + * T​r​a​y​ ​i​c​o​n​ ​t​h​e​m​e + */ + label: string + options: { + /** + * C​o​l​o​r + */ + color: string + /** + * W​h​i​t​e + */ + white: string + /** + * B​l​a​c​k + */ + black: string + /** + * G​r​a​y + */ + gray: string + } + } + logging: { + /** + * L​o​g​g​i​n​g​ ​t​h​r​e​s​h​o​l​d + */ + title: string + options: { + /** + * E​r​r​o​r + */ + error: string + /** + * I​n​f​o + */ + info: string + /** + * D​e​b​u​g + */ + debug: string + /** + * T​r​a​c​e + */ + trace: string + } + } + theme: { + /** + * T​h​e​m​e + */ + title: string + options: { + /** + * L​i​g​h​t + */ + light: string + /** + * D​a​r​k + */ + dark: string + } + } + } + } + } instancePage: { /** * L​o​c​a​t​i​o​n​s @@ -164,7 +242,11 @@ type RootTranslation = { */ label: string /** - * <​p​>​A​l​l​o​w​e​d​ ​t​r​a​f​f​i​c​:​<​/​b​r​>​ ​O​n​l​y​ ​t​r​a​f​i​c​ ​t​h​a​t​ ​w​a​s​ ​d​e​f​i​n​e​d​ ​b​y​ ​A​d​m​i​n​ ​f​o​r​ ​t​h​i​s​ ​l​o​c​a​t​i​o​n​.​<​/​p​> + * + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​p​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​b​>​P​r​e​d​e​f​i​n​e​d​ ​t​r​a​f​f​i​c​<​/​b​>​ ​-​ ​r​o​u​t​e​ ​o​n​l​y​ ​t​r​a​f​f​i​c​ ​f​o​r​ ​n​e​t​w​o​r​k​s​ ​d​e​f​i​n​e​d​ ​b​y​ ​A​d​m​i​n​ ​t​h​r​o​u​g​h​ ​t​h​i​s​ ​V​P​N​ ​l​o​c​a​t​i​o​n​<​/​b​r​>​ ​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​b​>​A​l​l​ ​t​r​a​f​f​i​c​<​/​b​>​ ​-​ ​r​o​u​t​e​ ​A​L​L​ ​y​o​u​r​ ​n​e​t​w​o​r​k​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​i​s​ ​V​P​N​ ​l​o​c​a​t​i​o​n​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​/​p​> */ helper: string } @@ -360,6 +442,10 @@ type RootTranslation = { * A​d​d​ ​I​n​s​t​a​n​c​e */ addInstance: string + /** + * S​e​t​t​i​n​g​s + */ + settings: string copyright: { /** * C​o​p​y​r​i​g​h​t​ ​©​ ​2​0​2​3 @@ -857,6 +943,84 @@ export type TranslationFunctions = { pages: { client: { pages: { + settingsPage: { + /** + * Settings + */ + title: () => LocalizedString + tabs: { + global: { + tray: { + /** + * System tray + */ + title: () => LocalizedString + /** + * Tray icon theme + */ + label: () => LocalizedString + options: { + /** + * Color + */ + color: () => LocalizedString + /** + * White + */ + white: () => LocalizedString + /** + * Black + */ + black: () => LocalizedString + /** + * Gray + */ + gray: () => LocalizedString + } + } + logging: { + /** + * Logging threshold + */ + title: () => LocalizedString + options: { + /** + * Error + */ + error: () => LocalizedString + /** + * Info + */ + info: () => LocalizedString + /** + * Debug + */ + debug: () => LocalizedString + /** + * Trace + */ + trace: () => LocalizedString + } + } + theme: { + /** + * Theme + */ + title: () => LocalizedString + options: { + /** + * Light + */ + light: () => LocalizedString + /** + * Dark + */ + dark: () => LocalizedString + } + } + } + } + } instancePage: { /** * Locations @@ -885,7 +1049,11 @@ export type TranslationFunctions = { */ label: () => LocalizedString /** - *

Allowed traffic:
Only trafic that was defined by Admin for this location.

+ * +

+ Predefined traffic - route only traffic for networks defined by Admin through this VPN location
+ All traffic - route ALL your network traffic through this VPN location +

*/ helper: () => LocalizedString } @@ -1081,6 +1249,10 @@ export type TranslationFunctions = { * Add Instance */ addInstance: () => LocalizedString + /** + * Settings + */ + settings: () => LocalizedString copyright: { /** * Copyright © 2023 diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index 407893e2..e7e1b0ee 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -10,6 +10,7 @@ import { RoutingRequest, SaveConfigRequest, SaveDeviceConfigResponse, + Settings, StatsRequest, TauriCommandKey, } from './types'; @@ -20,7 +21,6 @@ async function invokeWrapper( args?: InvokeArgs, timeout: number = 5000, ): Promise { - console.log(`Invoking command ${command}`); debug(`Invoking command '${command}'`); try { const res = await pTimeout(invoke(command, args), { @@ -65,6 +65,11 @@ const getActiveConnection = async (data: ConnectionRequest): Promise const updateLocationRouting = async (data: RoutingRequest): Promise => invokeWrapper('update_location_routing', data); +const getSettings = async (): Promise => invokeWrapper('get_settings'); + +const updateSettings = async (data: Partial): Promise => + invokeWrapper('update_settings', { data }); + export const clientApi = { getInstances, getLocations, @@ -76,4 +81,6 @@ export const clientApi = { getActiveConnection, saveConfig, updateLocationRouting, + getSettings, + updateSettings, }; diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 2ff59bf5..5bc800f3 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -1,3 +1,4 @@ +import { ThemeKey } from '../../../shared/defguard-ui/hooks/theme/types'; import { CreateDeviceResponse } from '../../../shared/hooks/api/types'; import { DefguardInstance, DefguardLocation } from '../types'; @@ -29,6 +30,16 @@ export type SaveDeviceConfigResponse = { locations: DefguardLocation[]; }; +export type TrayIconTheme = 'color' | 'white' | 'black' | 'gray'; + +export type LogLevel = 'error' | 'info' | 'debug' | 'trace'; + +export type Settings = { + theme: ThemeKey; + log_level: LogLevel; + tray_icon_theme: TrayIconTheme; +}; + export type TauriCommandKey = | 'all_instances' | 'all_locations' @@ -39,4 +50,6 @@ export type TauriCommandKey = | 'all_connections' | 'active_connection' | 'save_device_config' - | 'update_location_routing'; + | 'update_location_routing' + | 'get_settings' + | 'update_settings'; diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index f3ae4689..a24bb408 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -1,6 +1,7 @@ import './style.scss'; -import { useNavigate } from 'react-router-dom'; +import classNames from 'classnames'; +import { useMatch, useNavigate } from 'react-router-dom'; import { useI18nContext } from '../../../../i18n/i18n-react'; import SvgDefguadNavLogoCollapsed from '../../../../shared/components/svg/DefguardLogoCollapsed'; @@ -9,6 +10,7 @@ import SvgDefguardLogoText from '../../../../shared/components/svg/DefguardLogoT import SvgIconNavConnections from '../../../../shared/components/svg/IconNavConnections'; import { IconContainer } from '../../../../shared/defguard-ui/components/Layout/IconContainer/IconContainer'; import SvgIconPlus from '../../../../shared/defguard-ui/components/svg/IconPlus'; +import SvgIconSettings from '../../../../shared/defguard-ui/components/svg/IconSettings'; import { routes } from '../../../../shared/routes'; import { useClientStore } from '../../hooks/useClientStore'; import { ClientBarItem } from './components/ClientBarItem/ClientBarItem'; @@ -35,11 +37,32 @@ export const ClientSideBar = () => { ))} + ); }; +const SettingsNav = () => { + const { LL } = useI18nContext(); + const navigate = useNavigate(); + const pathActive = useMatch(routes.client.settings); + return ( +
{ + navigate(routes.client.settings, { replace: true }); + }} + > + +

{LL.pages.client.sideBar.settings()}

+
+ ); +}; + const AddInstance = () => { const { LL } = useI18nContext(); const navigate = useNavigate(); diff --git a/src/pages/client/components/ClientSideBar/style.scss b/src/pages/client/components/ClientSideBar/style.scss index ac55b290..d16418a2 100644 --- a/src/pages/client/components/ClientSideBar/style.scss +++ b/src/pages/client/components/ClientSideBar/style.scss @@ -5,12 +5,15 @@ position: fixed; inset: 0; height: 100%; - max-height: 100%; + max-height: 100vh; + max-height: 100dvh; overflow-x: hidden; overflow-y: auto; width: 70px; background-color: var(--surface-nav-bg); border-right: 1px solid var(--border-primary); + display: flex; + flex-flow: column; @include media-breakpoint-up(lg) { width: 270px; @@ -52,6 +55,8 @@ & > .items { display: flex; + flex-grow: 1; + flex-shrink: 0; flex-flow: column; align-items: flex-start; justify-content: flex-start; @@ -201,6 +206,10 @@ } } + #settings-nav-item { + margin-top: auto; + } + #add-instance { @include media-breakpoint-down(lg) { display: grid; diff --git a/src/pages/client/hooks/useClientStore.tsx b/src/pages/client/hooks/useClientStore.tsx index 75ebdd2e..f8eab15b 100644 --- a/src/pages/client/hooks/useClientStore.tsx +++ b/src/pages/client/hooks/useClientStore.tsx @@ -2,9 +2,10 @@ import { isUndefined } from 'lodash-es'; import { createWithEqualityFn } from 'zustand/traditional'; import { clientApi } from '../clientAPI/clientApi'; +import { Settings } from '../clientAPI/types'; import { ClientView, DefguardInstance } from '../types'; -const { getInstances } = clientApi; +const { getInstances, updateSettings } = clientApi; // eslint-disable-next-line const defaultValues: StoreValues = { @@ -12,6 +13,11 @@ const defaultValues: StoreValues = { selectedInstance: undefined, statsFilter: 1, selectedView: ClientView.GRID, + settings: { + log_level: 'error', + theme: 'light', + tray_icon_theme: 'color', + }, }; export const useClientStore = createWithEqualityFn( @@ -38,6 +44,10 @@ export const useClientStore = createWithEqualityFn( } set({ instances: res, selectedInstance: selected }); }, + updateSettings: async (data) => { + const res = await updateSettings(data); + set({ settings: res }); + }, }), Object.is, ); @@ -48,6 +58,7 @@ type StoreValues = { instances: DefguardInstance[]; selectedView: ClientView; statsFilter: number; + settings: Settings; selectedInstance?: DefguardInstance['id']; }; @@ -55,4 +66,5 @@ type StoreMethods = { setState: (values: Partial) => void; setInstances: (instances: DefguardInstance[]) => void; updateInstances: () => Promise; + updateSettings: (data: Partial) => Promise; }; diff --git a/src/pages/client/pages/ClientSettingsPage/ClientSettingsPage.tsx b/src/pages/client/pages/ClientSettingsPage/ClientSettingsPage.tsx new file mode 100644 index 00000000..2f5f0575 --- /dev/null +++ b/src/pages/client/pages/ClientSettingsPage/ClientSettingsPage.tsx @@ -0,0 +1,20 @@ +import './style.scss'; + +import { useI18nContext } from '../../../../i18n/i18n-react'; +import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { GlobalSettingsTab } from './components/GlobalSettingsTab/GlobalSettingsTab'; + +export const ClientSettingsPage = () => { + const { LL } = useI18nContext(); + const pageLL = LL.pages.client.pages.settingsPage; + return ( +
+
+

{pageLL.title()}

+
+ + + +
+ ); +}; diff --git a/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx b/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx new file mode 100644 index 00000000..e4f80206 --- /dev/null +++ b/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx @@ -0,0 +1,221 @@ +import './style.scss'; + +import { useMutation } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + +import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { Select } from '../../../../../../shared/defguard-ui/components/Layout/Select/Select'; +import { + SelectOption, + SelectSelectedValue, + SelectSizeVariant, +} from '../../../../../../shared/defguard-ui/components/Layout/Select/types'; +import { ThemeKey } from '../../../../../../shared/defguard-ui/hooks/theme/types'; +import { LogLevel, TrayIconTheme } from '../../../../clientAPI/types'; +import { useClientStore } from '../../../../hooks/useClientStore'; + +export const GlobalSettingsTab = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.settingsPage.tabs.global; + + return ( +
+
+

{localLL.tray.title()}

+ +
+
+

{localLL.logging.title()}

+ +
+
+

{localLL.theme.title()}

+ +
+
+ ); +}; + +const ThemeSelect = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.settingsPage.tabs.global.theme; + const settings = useClientStore((state) => state.settings); + const updateClientSettings = useClientStore((state) => state.updateSettings); + const { mutate, isPending } = useMutation({ + mutationFn: updateClientSettings, + }); + + const options = useMemo((): SelectOption[] => { + const res: SelectOption[] = [ + { + key: 0, + label: localLL.options.light(), + value: 'light', + }, + { + key: 1, + label: localLL.options.dark(), + value: 'dark', + }, + ]; + return res; + }, [localLL.options]); + + const renderSelected = useCallback( + (theme: ThemeKey): SelectSelectedValue => { + const option = options.find((o) => o.value === theme); + if (option) { + return { + key: option.key, + displayValue: option.label, + }; + } + return { + key: 999, + displayValue: '', + }; + }, + [options], + ); + + return ( + mutate({ log_level: level })} + /> + ); +}; + +const TrayIconThemeSelect = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.settingsPage.tabs.global; + const settings = useClientStore((state) => state.settings); + const updateClientSettings = useClientStore((state) => state.updateSettings); + + const { mutate, isPending } = useMutation({ + mutationFn: updateClientSettings, + }); + + const trayThemeSelectOptions = useMemo((): SelectOption[] => { + const res: SelectOption[] = [ + { + label: localLL.tray.options.color(), + value: 'color', + key: 0, + }, + { + value: 'gray', + key: 1, + label: localLL.tray.options.gray(), + }, + { + value: 'white', + key: 2, + label: localLL.tray.options.white(), + }, + { + value: 'black', + key: 3, + label: localLL.tray.options.black(), + }, + ]; + return res; + }, [localLL.tray.options]); + + const renderSelectedTrayTheme = useCallback( + (theme: TrayIconTheme): SelectSelectedValue => { + const option = trayThemeSelectOptions.find((t) => t.value === theme); + if (option) { + return { + key: option.key, + displayValue: option.label, + }; + } + return { + key: 'color', + displayValue: 'color', + }; + }, + [trayThemeSelectOptions], + ); + + return ( + { + setSelected(res); + onChange(res); + }} + /> + ); +}; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/style.scss b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/style.scss index 61caafe7..64b82459 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/style.scss +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/style.scss @@ -11,7 +11,13 @@ padding: 10px 20px; border-bottom: 1px solid var(--border-primary); - :nth-child(2) { + .select { + width: 100%; + max-width: 110px; + margin-left: 10px; + } + + :nth-child(3) { margin-left: auto; } } From 1d189af0e764f41aa9ebfed7d9d382545e975dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Fri, 29 Dec 2023 14:12:33 +0100 Subject: [PATCH 13/45] chore: update fs permissions for resources dir (#138) --- src-tauri/src/commands.rs | 15 ++++++++++++++- src-tauri/tauri.conf.json | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 65b93ec9..1baede66 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -564,10 +564,23 @@ pub async fn get_settings(handle: AppHandle) -> Result { pub async fn update_settings(data: SettingsPatch, handle: AppHandle) -> Result { let app_state = handle.state::(); let pool = &app_state.get_pool(); + trace!("Pool received"); let mut settings = Settings::get(pool).await?; + trace!("Settings read from table"); settings.apply(data); settings.save(pool).await?; - configure_tray_icon(&handle, &settings.tray_icon_theme)?; + debug!("Settings saved, reconfiguring tray icon."); + match configure_tray_icon(&handle, &settings.tray_icon_theme) { + Ok(_) => {} + Err(e) => { + error!( + "During settings update, tray configuration update failed. err : {}", + e.to_string() + ); + } + } + debug!("Tray icon updated"); + info!("Settings updated"); Ok(settings) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 95609b6a..5082abb2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -25,6 +25,9 @@ "all": true, "request": true, "scope": ["https://**", "http://**"] + }, + "fs": { + "scope": ["$RESOURCE/*"] } }, "bundle": { From 284855ccb27e5130e1ea6e30dcdf5a6b60405b74 Mon Sep 17 00:00:00 2001 From: Filip Slezak Date: Fri, 29 Dec 2023 14:15:02 +0100 Subject: [PATCH 14/45] chore: update tauri fs permissions --- src-tauri/tauri.conf.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5082abb2..4b103645 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -27,7 +27,8 @@ "scope": ["https://**", "http://**"] }, "fs": { - "scope": ["$RESOURCE/*"] + "all": true, + "scope": ["$RESOURCE/*", "$APPDATA/*"] } }, "bundle": { From 7bf1eacac67c2838be0d3ac862895f79659a46c5 Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Tue, 2 Jan 2024 09:20:09 +0100 Subject: [PATCH 15/45] feat: Tunnel grid view (#139) --- ...3768b2658406575d325fb273f152fe15465c4.json | 44 +++++++++++++ ...524020d8de92ef857028f1099d8fc0b2b72c6.json | 20 ++++++ ...24f0ca29a6ed2a9c328b2232c9cd90f0b3c04.json | 44 +++++++++++++ src-tauri/Cargo.toml | 2 +- src-tauri/src/bin/defguard-client.rs | 16 ++--- src-tauri/src/commands.rs | 34 +++++++++- src-tauri/src/database/models/instance.rs | 2 +- src-tauri/src/database/models/tunnel.rs | 62 +++++++++++++++++++ src-tauri/src/lib.rs | 5 ++ src/components/App/App.tsx | 10 ++- src/i18n/en/index.ts | 3 + src/i18n/i18n-types.ts | 12 ++++ src/pages/client/clientAPI/clientApi.ts | 15 +++-- src/pages/client/clientAPI/types.ts | 17 +++++ .../ClientSideBar/ClientSideBar.tsx | 33 +++++++++- .../ClientBarItem/ClientBarItem.tsx | 52 +++++++++++++--- .../components/ClientSideBar/style.scss | 1 - src/pages/client/hooks/useClientStore.tsx | 27 +++++--- .../AddInstanceDeviceForm.tsx | 8 ++- .../AddInstanceInitForm.tsx | 8 ++- .../ClientInstancePage/ClientInstancePage.tsx | 2 +- .../LocationsList/LocationsList.tsx | 42 +++++++++---- .../LocationCardConnectButton.tsx | 4 +- .../LocationCardInfo/LocationCardInfo.tsx | 4 +- .../LocationCardRoute/LocationCardRoute.tsx | 4 +- .../LocationCardTitle/LocationCardTitle.tsx | 4 +- .../LocationsDetailView.tsx | 11 ++-- .../LocationDetailCard/LocationDetailCard.tsx | 4 +- .../LocationsGridView/LocationsGridView.tsx | 11 ++-- .../ClientTunnelPage/ClientTunnelPage.tsx | 36 +++++++++++ .../client/pages/ClientTunnelPage/style.scss | 0 src/pages/client/query.ts | 1 + src/pages/client/types.ts | 38 ++++++++---- src/shared/routes.ts | 1 + 34 files changed, 491 insertions(+), 86 deletions(-) create mode 100644 src-tauri/.sqlx/query-19c18356163baa6db88272f09d83768b2658406575d325fb273f152fe15465c4.json create mode 100644 src-tauri/.sqlx/query-3ed7616493993dbd3e9f5c52da1524020d8de92ef857028f1099d8fc0b2b72c6.json create mode 100644 src-tauri/.sqlx/query-5476f3c222bfd9a65ec67af7f7224f0ca29a6ed2a9c328b2232c9cd90f0b3c04.json create mode 100644 src/pages/client/pages/ClientTunnelPage/ClientTunnelPage.tsx create mode 100644 src/pages/client/pages/ClientTunnelPage/style.scss diff --git a/src-tauri/.sqlx/query-19c18356163baa6db88272f09d83768b2658406575d325fb273f152fe15465c4.json b/src-tauri/.sqlx/query-19c18356163baa6db88272f09d83768b2658406575d325fb273f152fe15465c4.json new file mode 100644 index 00000000..7bfb0211 --- /dev/null +++ b/src-tauri/.sqlx/query-19c18356163baa6db88272f09d83768b2658406575d325fb273f152fe15465c4.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT id, tunnel_id, connected_from, start, end \n FROM tunnel_connection\n WHERE tunnel_id = $1\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "tunnel_id", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "connected_from", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "start", + "ordinal": 3, + "type_info": "Datetime" + }, + { + "name": "end", + "ordinal": 4, + "type_info": "Datetime" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "19c18356163baa6db88272f09d83768b2658406575d325fb273f152fe15465c4" +} diff --git a/src-tauri/.sqlx/query-3ed7616493993dbd3e9f5c52da1524020d8de92ef857028f1099d8fc0b2b72c6.json b/src-tauri/.sqlx/query-3ed7616493993dbd3e9f5c52da1524020d8de92ef857028f1099d8fc0b2b72c6.json new file mode 100644 index 00000000..33b9f1cb --- /dev/null +++ b/src-tauri/.sqlx/query-3ed7616493993dbd3e9f5c52da1524020d8de92ef857028f1099d8fc0b2b72c6.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO tunnel_connection (tunnel_id, connected_from, start, end) VALUES ($1, $2, $3, $4) RETURNING id;", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 4 + }, + "nullable": [ + false + ] + }, + "hash": "3ed7616493993dbd3e9f5c52da1524020d8de92ef857028f1099d8fc0b2b72c6" +} diff --git a/src-tauri/.sqlx/query-5476f3c222bfd9a65ec67af7f7224f0ca29a6ed2a9c328b2232c9cd90f0b3c04.json b/src-tauri/.sqlx/query-5476f3c222bfd9a65ec67af7f7224f0ca29a6ed2a9c328b2232c9cd90f0b3c04.json new file mode 100644 index 00000000..45053f26 --- /dev/null +++ b/src-tauri/.sqlx/query-5476f3c222bfd9a65ec67af7f7224f0ca29a6ed2a9c328b2232c9cd90f0b3c04.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT id, tunnel_id, connected_from, start, end\n FROM tunnel_connection\n WHERE tunnel_id = $1\n ORDER BY end DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "tunnel_id", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "connected_from", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "start", + "ordinal": 3, + "type_info": "Datetime" + }, + { + "name": "end", + "ordinal": 4, + "type_info": "Datetime" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "5476f3c222bfd9a65ec67af7f7224f0ca29a6ed2a9c328b2232c9cd90f0b3c04" +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c9be2ed0..480d655d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,7 +39,7 @@ x25519-dalek = { version = "2", features = [ strum = { version = "0.25", features = ["derive"] } dark-light = "1.0" -tauri = { version = "1.5", features = [ "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png"] } +tauri = { version = "1.5", features = [ "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png", "fs-all"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 1b40e05e..5f0343eb 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -12,15 +12,16 @@ use tauri_plugin_log::LogTarget; use defguard_client::{ __cmd__active_connection, __cmd__all_connections, __cmd__all_instances, __cmd__all_locations, - __cmd__connect, __cmd__delete_instance, __cmd__disconnect, __cmd__get_settings, - __cmd__last_connection, __cmd__location_interface_details, __cmd__location_stats, - __cmd__parse_tunnel_config, __cmd__save_device_config, __cmd__save_tunnel, - __cmd__update_instance, __cmd__update_location_routing, __cmd__update_settings, + __cmd__all_tunnels, __cmd__connect, __cmd__delete_instance, __cmd__disconnect, + __cmd__get_settings, __cmd__last_connection, __cmd__location_interface_details, + __cmd__location_stats, __cmd__parse_tunnel_config, __cmd__save_device_config, + __cmd__save_tunnel, __cmd__update_instance, __cmd__update_location_routing, + __cmd__update_settings, appstate::AppState, commands::{ - active_connection, all_connections, all_instances, all_locations, connect, delete_instance, - disconnect, get_settings, last_connection, location_interface_details, location_stats, - parse_tunnel_config, save_device_config, save_tunnel, update_instance, + active_connection, all_connections, all_instances, all_locations, all_tunnels, connect, + delete_instance, disconnect, get_settings, last_connection, location_interface_details, + location_stats, parse_tunnel_config, save_device_config, save_tunnel, update_instance, update_location_routing, update_settings, }, database::{self, models::settings::Settings}, @@ -92,6 +93,7 @@ async fn main() { delete_instance, parse_tunnel_config, save_tunnel, + all_tunnels, ]) .on_window_event(|event| match event.event() { tauri::WindowEvent::CloseRequested { api, .. } => { diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1baede66..a73724fb 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -260,7 +260,7 @@ pub async fn all_instances(app_state: State<'_, AppState>) -> Result) -> info!("Saved tunnel {tunnel:#?}"); Ok(()) } + +#[derive(Debug, Serialize, Deserialize)] +pub struct TunnelInfo { + pub id: Option, + pub name: String, + pub address: String, + pub endpoint: String, + pub active: bool, + pub route_all_traffic: bool, +} + +#[tauri::command(async)] +pub async fn all_tunnels(app_state: State<'_, AppState>) -> Result, Error> { + debug!("Retrieving all instances."); + + let tunnels = Tunnel::all(&app_state.get_pool()).await?; + debug!("Found ({}) tunnels", tunnels.len()); + trace!("Instances found: {tunnels:#?}"); + let mut tunnel_info: Vec = vec![]; + + for tunnel in tunnels { + tunnel_info.push(TunnelInfo { + id: tunnel.id, + name: tunnel.name, + address: tunnel.address, + endpoint: tunnel.endpoint, + route_all_traffic: tunnel.route_all_traffic, + active: false, + }) + } + Ok(tunnel_info) +} diff --git a/src-tauri/src/database/models/instance.rs b/src-tauri/src/database/models/instance.rs index 61e97243..b162ee92 100644 --- a/src-tauri/src/database/models/instance.rs +++ b/src-tauri/src/database/models/instance.rs @@ -97,6 +97,6 @@ pub struct InstanceInfo { pub name: String, pub uuid: String, pub url: String, - pub connected: bool, + pub active: bool, pub pubkey: String, } diff --git a/src-tauri/src/database/models/tunnel.rs b/src-tauri/src/database/models/tunnel.rs index 406b0111..f4c1ad55 100644 --- a/src-tauri/src/database/models/tunnel.rs +++ b/src-tauri/src/database/models/tunnel.rs @@ -275,3 +275,65 @@ pub async fn peer_to_tunnel_stats( persistent_keepalive_interval: peer.persistent_keepalive_interval, }) } + +#[derive(FromRow, Debug, Serialize, Clone)] +pub struct TunnelConnection { + pub id: Option, + pub tunnel_id: i64, + pub connected_from: String, + pub start: NaiveDateTime, + pub end: NaiveDateTime, +} + +impl TunnelConnection { + pub async fn save(&mut self, pool: &DbPool) -> Result<(), Error> { + let result = query!( + "INSERT INTO tunnel_connection (tunnel_id, connected_from, start, end) \ + VALUES ($1, $2, $3, $4) \ + RETURNING id;", + self.tunnel_id, + self.connected_from, + self.start, + self.end, + ) + .fetch_one(pool) + .await?; + self.id = Some(result.id); + Ok(()) + } + + pub async fn all_by_tunnel_id(pool: &DbPool, tunnel_id: i64) -> Result, Error> { + let connections = query_as!( + TunnelConnection, + r#" + SELECT id, tunnel_id, connected_from, start, end + FROM tunnel_connection + WHERE tunnel_id = $1 + "#, + tunnel_id + ) + .fetch_all(pool) + .await?; + Ok(connections) + } + + pub async fn lastest_by_tunnel_id( + pool: &DbPool, + tunnel_id: i64, + ) -> Result, Error> { + let connection = query_as!( + TunnelConnection, + r#" + SELECT id, tunnel_id, connected_from, start, end + FROM tunnel_connection + WHERE tunnel_id = $1 + ORDER BY end DESC + LIMIT 1 + "#, + tunnel_id + ) + .fetch_optional(pool) + .await?; + Ok(connection) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0987eb68..caebda14 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,5 +13,10 @@ struct Payload { cwd: String, } +pub enum ConnectionType { + Tunnel, + Location, +} + #[macro_use] extern crate log; diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 7af314c9..11a86d92 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -27,6 +27,7 @@ import { ClientAddInstancePage } from '../../pages/client/pages/ClientAddInstanc import { ClientAddTunnelPage } from '../../pages/client/pages/ClientAddTunnelPage/ClientAddTunnelPage'; import { ClientInstancePage } from '../../pages/client/pages/ClientInstancePage/ClientInstancePage'; import { ClientSettingsPage } from '../../pages/client/pages/ClientSettingsPage/ClientSettingsPage'; +import { ClientTunnelPage } from '../../pages/client/pages/ClientTunnelPage/ClientTunnelPage'; import { EnrollmentPage } from '../../pages/enrollment/EnrollmentPage'; import { SessionTimeoutPage } from '../../pages/sessionTimeout/SessionTimeoutPage'; import { ToastManager } from '../../shared/defguard-ui/components/Layout/ToastManager/ToastManager'; @@ -43,7 +44,7 @@ dayjs.extend(timezone); const queryClient = new QueryClient(); -const { getSettings, getInstances } = clientApi; +const { getSettings, getInstances, getTunnels } = clientApi; const router = createBrowserRouter([ { @@ -75,6 +76,10 @@ const router = createBrowserRouter([ path: '/client/add-tunnel', element: , }, + { + path: '/client/tunnel', + element: , + }, { path: '/client/settings', element: , @@ -119,7 +124,8 @@ export const App = () => { debug('App init state from tauri'); const settings = await getSettings(); const instances = await getInstances(); - setClientState({ settings, instances }); + const tunnels = await getTunnels(); + setClientState({ settings, instances, tunnels }); debug('Tauri init data loaded'); setSettingsLoaded(true); }; diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 56c42d56..1a83e0f4 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -166,6 +166,9 @@ const en = { }, }, }, + tunnelPage: { + title: 'WireGuard Tunnels', + }, addTunnelPage: { title: 'Add WireGuard® Tunnel', forms: { diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index d5f088e0..1c42ab0b 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -417,6 +417,12 @@ type RootTranslation = { } } } + tunnelPage: { + /** + * W​i​r​e​G​u​a​r​d​ ​T​u​n​n​e​l​s + */ + title: string + } addTunnelPage: { /** * A​d​d​ ​W​i​r​e​G​u​a​r​d​®​ ​T​u​n​n​e​l @@ -1598,6 +1604,12 @@ export type TranslationFunctions = { } } } + tunnelPage: { + /** + * WireGuard Tunnels + */ + title: () => LocalizedString + } addTunnelPage: { /** * Add WireGuard® Tunnel diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index f2fda031..4d448c35 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -4,11 +4,10 @@ import pTimeout from 'p-timeout'; import { debug, error, trace } from 'tauri-plugin-log-api'; import { + CommonWireguardFields, Connection, DefguardInstance, - DefguardLocation, LocationStats, - Tunnel, } from '../types'; import { ConnectionRequest, @@ -21,6 +20,7 @@ import { Settings, StatsRequest, TauriCommandKey, + TunnelRequest, UpdateInstnaceRequest, } from './types'; @@ -50,8 +50,9 @@ const saveConfig = async (data: SaveConfigRequest): Promise => invokeWrapper('all_instances'); -const getLocations = async (data: GetLocationsRequest): Promise => - invokeWrapper('all_locations', data); +const getLocations = async ( + data: GetLocationsRequest, +): Promise => invokeWrapper('all_locations', data); const connect = async (data: ConnectionRequest): Promise => invokeWrapper('connect', data); @@ -88,15 +89,19 @@ const updateInstance = async (data: UpdateInstnaceRequest): Promise => const parseTunnelConfig = async (config: string) => invokeWrapper('parse_tunnel_config', { config: config }); -const saveTunnel = async (tunnel: Tunnel) => +const saveTunnel = async (tunnel: TunnelRequest) => invokeWrapper('save_tunnel', { tunnel: tunnel }); const getLocationDetails = async ( data: LocationDetailsRequest, ): Promise => invokeWrapper('location_interface_details', data); +const getTunnels = async (): Promise => + invokeWrapper('all_tunnels'); + export const clientApi = { getInstances, + getTunnels, getLocations, connect, disconnect, diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 6ad769f7..cbd29899 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -80,6 +80,22 @@ export type LocationDetails = { last_handshake?: number; }; +export type TunnelRequest = { + name: string; + pubkey: string; + prvkey: string; + address: string; + server_pubkey: string; + allowed_ips?: string; + endpoint: string; + dns?: string; + persistent_keep_alive: number; + pre_up?: string; + post_up?: string; + pre_down?: string; + post_down?: string; +}; + export type LocationDetailsRequest = { locationId: number; }; @@ -101,4 +117,5 @@ export type TauriCommandKey = | 'update_instance' | 'parse_tunnel_config' | 'save_tunnel' + | 'all_tunnels' | 'location_interface_details'; diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index f17646b8..2d4398f7 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -14,11 +14,17 @@ import SvgIconPlus from '../../../../shared/defguard-ui/components/svg/IconPlus' import SvgIconSettings from '../../../../shared/defguard-ui/components/svg/IconSettings'; import { routes } from '../../../../shared/routes'; import { useClientStore } from '../../hooks/useClientStore'; +import { WireguardInstanceType } from '../../types'; import { ClientBarItem } from './components/ClientBarItem/ClientBarItem'; export const ClientSideBar = () => { + const navigate = useNavigate(); const { LL } = useI18nContext(); - const instances = useClientStore((state) => state.instances); + const [instances, tunnels, setClientStore] = useClientStore((state) => [ + state.instances, + state.tunnels, + state.setState, + ]); return (
@@ -35,13 +41,34 @@ export const ClientSideBar = () => {

{LL.pages.client.sideBar.instances()}

{instances.map((instance) => ( - + ))} -
+
{ + setClientStore({ + selectedInstance: { + id: undefined, + type: WireguardInstanceType.TUNNEL, + }, + }); + navigate(routes.client.tunnelPage, { replace: true }); + }} + >

{LL.pages.client.sideBar.tunnels()}

+ {tunnels.map((tunnel) => ( + + ))}
diff --git a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx index 7acdf16e..02fe316d 100644 --- a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx +++ b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx @@ -5,20 +5,37 @@ import { useMatch, useNavigate } from 'react-router-dom'; import SvgIconConnection from '../../../../../../shared/defguard-ui/components/svg/IconConnection'; import { routes } from '../../../../../../shared/routes'; import { useClientStore } from '../../../../hooks/useClientStore'; -import { DefguardInstance } from '../../../../types'; +import { WireguardInstanceType } from '../../../../types'; -type Props = { - instance: DefguardInstance; +interface BaseInstance { + id?: number; + name: string; + // Connected + active: boolean; + type: WireguardInstanceType; +} + +type Props = { + instance: T; }; -export const ClientBarItem = ({ instance }: Props) => { +export const ClientBarItem = ({ instance }: Props) => { const instancePage = useMatch('/client/'); const navigate = useNavigate(); const setClientStore = useClientStore((state) => state.setState); const selectedInstance = useClientStore((state) => state.selectedInstance); + + // FIXME: Fix tunnel active when detail will be implemented + const active = + instance.type === WireguardInstanceType.TUNNEL + ? routes.client.tunnelPage + instance.id === window.location.pathname + : instance.type === WireguardInstanceType.DEFGUARD_INSTANCE + ? instance.id === selectedInstance?.id + : false; + const cn = classNames('client-bar-item', 'clickable', { - active: instance.id === selectedInstance, - connected: instance.connected, + active: active, + connected: instance.active, }); const { refs, floatingStyles } = useFloating({ @@ -33,9 +50,24 @@ export const ClientBarItem = ({ instance }: Props) => { className={cn} ref={refs.setReference} onClick={() => { - setClientStore({ selectedInstance: instance.id }); - if (!instancePage) { - navigate(routes.client.base, { replace: true }); + if (instance.type === WireguardInstanceType.DEFGUARD_INSTANCE) { + setClientStore({ + selectedInstance: { + id: instance.id as number, + type: WireguardInstanceType.DEFGUARD_INSTANCE, + }, + }); + if (!instancePage) { + navigate(routes.client.base, { replace: true }); + } + } else { + setClientStore({ + selectedInstance: { + id: instance.id as number, + type: WireguardInstanceType.TUNNEL, + }, + }); + navigate(routes.client.tunnelPage); } }} > @@ -46,7 +78,7 @@ export const ClientBarItem = ({ instance }: Props) => {

{instance.name[0]}

- {instance.connected && ( + {instance.active && (
.client-bar-item { display: grid; box-sizing: border-box; diff --git a/src/pages/client/hooks/useClientStore.tsx b/src/pages/client/hooks/useClientStore.tsx index f8eab15b..90180da8 100644 --- a/src/pages/client/hooks/useClientStore.tsx +++ b/src/pages/client/hooks/useClientStore.tsx @@ -3,13 +3,20 @@ import { createWithEqualityFn } from 'zustand/traditional'; import { clientApi } from '../clientAPI/clientApi'; import { Settings } from '../clientAPI/types'; -import { ClientView, DefguardInstance } from '../types'; +import { + ClientView, + CommonWireguardFields, + DefguardInstance, + SelectedInstance, + WireguardInstanceType, +} from '../types'; const { getInstances, updateSettings } = clientApi; // eslint-disable-next-line const defaultValues: StoreValues = { instances: [], + tunnels: [], selectedInstance: undefined, statsFilter: 1, selectedView: ClientView.GRID, @@ -26,7 +33,12 @@ export const useClientStore = createWithEqualityFn( setState: (values) => set({ ...values }), setInstances: (values) => { if (isUndefined(get().selectedInstance)) { - return set({ instances: values, selectedInstance: values[0]?.id ?? undefined }); + return set({ + instances: values, + selectedInstance: + { id: values[0]?.id, type: WireguardInstanceType.DEFGUARD_INSTANCE } ?? + undefined, + }); } return set({ instances: values }); }, @@ -34,13 +46,13 @@ export const useClientStore = createWithEqualityFn( const res = await getInstances(); let selected = get().selectedInstance; // check if currently selected instances is in updated instances - if (!isUndefined(selected) && res.length) { - if (!res.map((i) => i.id).includes(selected)) { - selected = res[0].id; + if (!isUndefined(selected) && res.length && selected.id) { + if (!res.map((i) => i.id).includes(selected.id)) { + selected = { id: res[0].id, type: WireguardInstanceType.DEFGUARD_INSTANCE }; } } if (isUndefined(selected) && res.length) { - selected = res[0].id; + selected = { id: res[0].id, type: WireguardInstanceType.DEFGUARD_INSTANCE }; } set({ instances: res, selectedInstance: selected }); }, @@ -56,10 +68,11 @@ type Store = StoreValues & StoreMethods; type StoreValues = { instances: DefguardInstance[]; + tunnels: CommonWireguardFields[]; selectedView: ClientView; statsFilter: number; settings: Settings; - selectedInstance?: DefguardInstance['id']; + selectedInstance?: SelectedInstance; }; type StoreMethods = { diff --git a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx index 49850671..865709c1 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx @@ -24,6 +24,7 @@ import { routes } from '../../../../../../../../shared/routes'; import { generateWGKeys } from '../../../../../../../../shared/utils/generateWGKeys'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; +import { WireguardInstanceType } from '../../../../../../types'; import { AddInstanceInitResponse } from '../../types'; const { saveConfig } = clientApi; @@ -102,7 +103,12 @@ export const AddInstanceDeviceForm = ({ response }: Props) => { .then((res) => { setIsLoading(false); toaster.success(localLL.messages.addSuccess()); - setClientStore({ selectedInstance: res.instance.id }); + setClientStore({ + selectedInstance: { + id: res.instance.id, + type: WireguardInstanceType.DEFGUARD_INSTANCE, + }, + }); navigate(routes.client.base, { replace: true }); }) .catch(() => { diff --git a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx index 1e940823..369582cb 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx @@ -26,6 +26,7 @@ import { routes } from '../../../../../../../../shared/routes'; import { useEnrollmentStore } from '../../../../../../../enrollment/hooks/store/useEnrollmentStore'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; +import { WireguardInstanceType } from '../../../../../../types'; import { AddInstanceInitResponse } from '../../types'; type Props = { @@ -140,7 +141,12 @@ export const AddInstanceInitForm = ({ nextStep }: Props) => { toaster.success( LL.pages.enrollment.steps.deviceSetup.desktopSetup.messages.deviceConfigured(), ); - setClientState({ selectedInstance: instance.id }); + setClientState({ + selectedInstance: { + id: instance.id, + type: WireguardInstanceType.DEFGUARD_INSTANCE, + }, + }); navigate(routes.client.base, { replace: true }); }) .catch((e) => { diff --git a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx index e085f1a8..e46a1cf3 100644 --- a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx +++ b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx @@ -19,7 +19,7 @@ export const ClientInstancePage = () => { const { LL } = useI18nContext(); const pageLL = LL.pages.client.pages.instancePage; const instances = useClientStore((state) => state.instances); - const selectedInstanceId = useClientStore((state) => state.selectedInstance); + const selectedInstanceId = useClientStore((state) => state.selectedInstance?.id); const selectedInstance = useMemo( () => instances.find((i) => i.id === selectedInstanceId), [instances, selectedInstanceId], diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx index fa83e2f6..0c017679 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx @@ -1,16 +1,16 @@ import { useQuery } from '@tanstack/react-query'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useI18nContext } from '../../../../../../i18n/i18n-react'; import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; import { clientApi } from '../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../query'; -import { ClientView } from '../../../../types'; +import { ClientView, WireguardInstanceType } from '../../../../types'; import { LocationsDetailView } from './components/LocationsDetailView/LocationsDetailView'; import { LocationsGridView } from './components/LocationsGridView/LocationsGridView'; -const { getLocations } = clientApi; +const { getLocations, getTunnels } = clientApi; export const LocationsList = () => { const { LL } = useI18nContext(); @@ -20,9 +20,25 @@ export const LocationsList = () => { const toaster = useToaster(); + const queryKey = useMemo(() => { + if (selectedInstance?.type === WireguardInstanceType.DEFGUARD_INSTANCE) { + return [clientQueryKeys.getLocations, selectedInstance?.id as number]; + } else { + return [clientQueryKeys.getTunnels]; + } + }, [selectedInstance]); + + const queryFn = useCallback(() => { + if (selectedInstance?.type === WireguardInstanceType.DEFGUARD_INSTANCE) { + return getLocations({ instanceId: selectedInstance?.id as number }); + } else { + return getTunnels(); + } + }, [selectedInstance]); + const { data: locations, isError } = useQuery({ - queryKey: [clientQueryKeys.getLocations, selectedInstance as number], - queryFn: () => getLocations({ instanceId: selectedInstance as number }), + queryKey, + queryFn, enabled: !!selectedInstance, }); @@ -37,12 +53,16 @@ export const LocationsList = () => { return ( <> - {selectedView === ClientView.GRID && ( - - )} - {selectedView === ClientView.DETAIL && ( - - )} + {selectedView === ClientView.GRID && + (selectedInstance.id || + selectedInstance.type === WireguardInstanceType.TUNNEL) && ( + + )} + {selectedView === ClientView.DETAIL && + selectedInstance.id && + selectedInstance.type === WireguardInstanceType.DEFGUARD_INSTANCE && ( + + )} ); }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx index 86302c14..490eeb58 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx @@ -14,12 +14,12 @@ import { import SvgIconX from '../../../../../../../../shared/defguard-ui/components/svg/IconX'; import { useToaster } from '../../../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; import { clientApi } from '../../../../../../clientAPI/clientApi'; -import { DefguardLocation } from '../../../../../../types'; +import { CommonWireguardFields } from '../../../../../../types'; const { connect, disconnect } = clientApi; type Props = { - location?: DefguardLocation; + location?: CommonWireguardFields; }; export const LocationCardConnectButton = ({ location }: Props) => { diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/LocationCardInfo.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/LocationCardInfo.tsx index 5eace993..c87bff9d 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/LocationCardInfo.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/LocationCardInfo.tsx @@ -6,10 +6,10 @@ import dayjs from 'dayjs'; import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { clientQueryKeys } from '../../../../../../query'; -import { Connection, DefguardLocation } from '../../../../../../types'; +import { CommonWireguardFields, Connection } from '../../../../../../types'; type Props = { - location?: DefguardLocation; + location?: CommonWireguardFields; connection?: Connection; }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx index 10207af6..66464d93 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx @@ -7,10 +7,10 @@ import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import { Toggle } from '../../../../../../../../shared/defguard-ui/components/Layout/Toggle/Toggle'; import { ToggleOption } from '../../../../../../../../shared/defguard-ui/components/Layout/Toggle/types'; import { clientApi } from '../../../../../../clientAPI/clientApi'; -import { DefguardLocation } from '../../../../../../types'; +import { CommonWireguardFields } from '../../../../../../types'; type Props = { - location?: DefguardLocation; + location?: CommonWireguardFields; }; const { updateLocationRouting } = clientApi; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardTitle/LocationCardTitle.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardTitle/LocationCardTitle.tsx index 6e817691..e2ca231b 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardTitle/LocationCardTitle.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardTitle/LocationCardTitle.tsx @@ -5,10 +5,10 @@ import classNames from 'classnames'; import { Badge } from '../../../../../../../../shared/defguard-ui/components/Layout/Badge/Badge'; import { BadgeStyleVariant } from '../../../../../../../../shared/defguard-ui/components/Layout/Badge/types'; import SvgIconConnection from '../../../../../../../../shared/defguard-ui/components/svg/IconConnection'; -import { DefguardLocation } from '../../../../../../types'; +import { CommonWireguardFields } from '../../../../../../types'; type Props = { - location?: DefguardLocation; + location?: CommonWireguardFields; }; export const LocationCardTitle = ({ location }: Props) => { diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx index 7c3abb2c..60aa6a0a 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx @@ -7,20 +7,19 @@ import { useNavigate } from 'react-router-dom'; import { CardTabs } from '../../../../../../../../shared/defguard-ui/components/Layout/CardTabs/CardTabs'; import { CardTabsData } from '../../../../../../../../shared/defguard-ui/components/Layout/CardTabs/types'; import { routes } from '../../../../../../../../shared/routes'; -import { DefguardInstance, DefguardLocation } from '../../../../../../types'; +import { CommonWireguardFields } from '../../../../../../types'; import { LocationConnectionHistory } from './components/LocationConnectionHistory/LocationConnectionHistory'; import { LocationDetailCard } from './components/LocationDetailCard/LocationDetailCard'; import { LocationDetails } from './components/LocationDetails/LocationDetails'; type Props = { - instanceId: DefguardInstance['id']; - locations: DefguardLocation[]; + locations: CommonWireguardFields[]; }; const findLocationById = ( - locations: DefguardLocation[], + locations: CommonWireguardFields[], id: number, -): DefguardLocation | undefined => locations.find((location) => location.id === id); +): CommonWireguardFields | undefined => locations.find((location) => location.id === id); export const LocationsDetailView = ({ locations }: Props) => { const [activeLocationId, setActiveLocationId] = useState( @@ -39,7 +38,7 @@ export const LocationsDetailView = ({ locations }: Props) => { [locations, activeLocationId], ); - const activeLocation = useMemo((): DefguardLocation | undefined => { + const activeLocation = useMemo((): CommonWireguardFields | undefined => { if (!isUndefined(activeLocationId)) { return findLocationById(locations, activeLocationId); } diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx index f94299ef..0728a033 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx @@ -15,7 +15,7 @@ import { getStatsFilterValue } from '../../../../../../../../../../shared/utils/ import { clientApi } from '../../../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../../../../../query'; -import { DefguardLocation } from '../../../../../../../../types'; +import { CommonWireguardFields } from '../../../../../../../../types'; import { LocationUsageChart } from '../../../../../LocationUsageChart/LocationUsageChart'; import { LocationUsageChartType } from '../../../../../LocationUsageChart/types'; import { LocationCardConnectButton } from '../../../LocationCardConnectButton/LocationCardConnectButton'; @@ -25,7 +25,7 @@ import { LocationCardRoute } from '../../../LocationCardRoute/LocationCardRoute' import { LocationCardTitle } from '../../../LocationCardTitle/LocationCardTitle'; type Props = { - location: DefguardLocation; + location: CommonWireguardFields; tabbed?: boolean; }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx index 1f1e68eb..838f9f1d 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx @@ -12,7 +12,7 @@ import { getStatsFilterValue } from '../../../../../../../../shared/utils/getSta import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../../../query'; -import { DefguardInstance, DefguardLocation } from '../../../../../../types'; +import { CommonWireguardFields } from '../../../../../../types'; import { LocationUsageChart } from '../../../LocationUsageChart/LocationUsageChart'; import { LocationUsageChartType } from '../../../LocationUsageChart/types'; import { LocationCardConnectButton } from '../LocationCardConnectButton/LocationCardConnectButton'; @@ -23,22 +23,21 @@ import { LocationCardRoute } from '../LocationCardRoute/LocationCardRoute'; import { LocationCardTitle } from '../LocationCardTitle/LocationCardTitle'; type Props = { - instanceId: DefguardInstance['id']; - locations: DefguardLocation[]; + locations: CommonWireguardFields[]; }; -export const LocationsGridView = ({ instanceId, locations }: Props) => { +export const LocationsGridView = ({ locations }: Props) => { return (
{locations.map((l) => ( - + ))}
); }; type GridItemProps = { - location: DefguardLocation; + location: CommonWireguardFields; }; const GridItem = ({ location }: GridItemProps) => { diff --git a/src/pages/client/pages/ClientTunnelPage/ClientTunnelPage.tsx b/src/pages/client/pages/ClientTunnelPage/ClientTunnelPage.tsx new file mode 100644 index 00000000..cd5fa96f --- /dev/null +++ b/src/pages/client/pages/ClientTunnelPage/ClientTunnelPage.tsx @@ -0,0 +1,36 @@ +import './style.scss'; + +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useI18nContext } from '../../../../i18n/i18n-react'; +import { routes } from '../../../../shared/routes'; +import { useClientStore } from '../../hooks/useClientStore'; +import { LocationsList } from '../ClientInstancePage/components/LocationsList/LocationsList'; +import { StatsFilterSelect } from '../ClientInstancePage/components/StatsFilterSelect/StatsFilterSelect'; + +export const ClientTunnelPage = () => { + const { LL } = useI18nContext(); + const pageLL = LL.pages.client.pages.tunnelPage; + const tunnels = useClientStore((state) => state.tunnels); + const navigate = useNavigate(); + + // router guard, if no tunnels redirect to add tunnel + useEffect(() => { + if (tunnels.length === 0) { + navigate(routes.client.addTunnel, { replace: true }); + } + }, [tunnels, navigate]); + + return ( +
+
+

{pageLL.title()}

+
+ +
+
+ +
+ ); +}; diff --git a/src/pages/client/pages/ClientTunnelPage/style.scss b/src/pages/client/pages/ClientTunnelPage/style.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/client/query.ts b/src/pages/client/query.ts index 285d5da7..620e2cc9 100644 --- a/src/pages/client/query.ts +++ b/src/pages/client/query.ts @@ -6,4 +6,5 @@ export const clientQueryKeys = { getConnectionHistory: 'GET_CONNECTIONS_HISTORY', getActiveConnection: 'GET_ACTIVE_CONNECTION', getLocationDetails: 'GET_LOCATION_DETAILS', + getTunnels: 'GET_TUNNELS', }; diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts index 4b4043d7..2ca92139 100644 --- a/src/pages/client/types.ts +++ b/src/pages/client/types.ts @@ -3,20 +3,14 @@ export type DefguardInstance = { uuid: string; name: string; url: string; - connected: boolean; + // connected + active: boolean; pubkey: string; }; export type DefguardLocation = { - id: number; instance_id: number; - name: string; - address: string; - endpoint: string; - // connected - active: boolean; - route_all_traffic: boolean; -}; +} & CommonWireguardFields; export type LocationStats = { collected_at: number; @@ -36,19 +30,29 @@ export type Connection = { export type Tunnel = { id?: number; - name: string; pubkey: string; prvkey: string; - address: string; server_pubkey: string; allowed_ips?: string; - endpoint: string; dns?: string; persistent_keep_alive: number; pre_up?: string; post_up?: string; pre_down?: string; post_down?: string; +} & CommonWireguardFields; + +// Common fields between Tunnel, Location and instance +// Shared between components as props to avoid component duplication +export type CommonWireguardFields = { + id: number; + name: string; + address: string; + endpoint: string; + route_all_traffic: boolean; + // Connected + active: boolean; + type?: WireguardInstanceType; }; export enum ClientView { @@ -56,6 +60,16 @@ export enum ClientView { DETAIL = 1, } +export enum WireguardInstanceType { + TUNNEL = 'Tunnel', + DEFGUARD_INSTANCE = 'Instance', +} + +export type SelectedInstance = { + id?: number; + type: WireguardInstanceType; +}; + export enum TauriEventKey { SINGLE_INSTANCE = 'single-instance', CONNECTION_CHANGED = 'connection-changed', diff --git a/src/shared/routes.ts b/src/shared/routes.ts index 868d0535..bc3c2d6b 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -6,6 +6,7 @@ export const routes = { instancePage: '/client/', addInstance: '/client/add-instance', addTunnel: '/client/add-tunnel', + tunnelPage: '/client/tunnel', settings: '/client/settings', }, enrollment: '/enrollment', From a7d466ec10b42518a44e50699478d287341214be Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 2 Jan 2024 11:34:34 +0100 Subject: [PATCH 16/45] feat: update instance & location info (#140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * reduce file log level for hyper * add new columns * update instance struct * post-merge fix * update structs * update query data * update dependencies * fix DB initialization * update protos * remove unused method * add more context --------- Co-authored-by: Maciej Wójcik --- ...2227ab0ed2df1a05397890de3a55d62d27ab.json} | 6 +- ...d82ebaa96f1d438388521b4761da5bb93ce4.json} | 16 +- ...333d32a45eee6b9a70bb74713bee2f6283321.json | 12 + ...7c90d16faf6c96ef650e6bc8d4e5e193e0855.json | 12 - ...427be3933d6ad26f685d1615fa2af0a09bc1.json} | 16 +- ...a3a58f804f5d59d14fa70a65dba68c079b01.json} | 16 +- ...d594750c967d6d30c3da7ce0e56fb089ea06.json} | 6 +- ...ca8c407ab7b15ac505968c66924df3563ff8.json} | 16 +- ...81a7011ca163d2511fdebf95d8cb79c17d7a.json} | 6 +- ...8e991f091f17e25583bd251347c207d8ff5c.json} | 16 +- ...20e028bc2609b39578f1ccd5bfa9df2240e8.json} | 16 +- ...dde7717aca865f082f24b500214814c4a3ef.json} | 16 +- src-tauri/Cargo.lock | 261 +++++++++--------- src-tauri/Cargo.toml | 24 +- src-tauri/build.rs | 7 +- .../20231228102040_add_more_instance_info.sql | 7 + src-tauri/proto | 2 +- src-tauri/src/appstate.rs | 15 +- src-tauri/src/bin/defguard-client.rs | 53 ++-- src-tauri/src/bin/defguard-service.rs | 10 +- src-tauri/src/commands.rs | 60 ++-- src-tauri/src/database/mod.rs | 6 +- src-tauri/src/database/models/instance.rs | 53 +++- src-tauri/src/database/models/location.rs | 95 +++---- src-tauri/src/lib.rs | 4 + 25 files changed, 439 insertions(+), 312 deletions(-) rename src-tauri/.sqlx/{query-b31daa7e35a53918352e0d3c4a5698368fd4a33bb5cafe5d2f680dc0ccf33f5a.json => query-2519ee530137aff4f6d567cfd0732227ab0ed2df1a05397890de3a55d62d27ab.json} (51%) rename src-tauri/.sqlx/{query-8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd.json => query-368814ed137d486c51e412c386ccd82ebaa96f1d438388521b4761da5bb93ce4.json} (74%) create mode 100644 src-tauri/.sqlx/query-68772513090d1bd3fbf58546971333d32a45eee6b9a70bb74713bee2f6283321.json delete mode 100644 src-tauri/.sqlx/query-807d90d55c57bf87c224a5d69357c90d16faf6c96ef650e6bc8d4e5e193e0855.json rename src-tauri/.sqlx/{query-8c85b8c4cfd2b85ff96db3ce58ce727b3c77039d5984f523bb1923c8b81cbcd1.json => query-89618232479b662d911715e0c9de427be3933d6ad26f685d1615fa2af0a09bc1.json} (74%) rename src-tauri/.sqlx/{query-fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86.json => query-8dfaa1c23af01a966442b52412eaa3a58f804f5d59d14fa70a65dba68c079b01.json} (74%) rename src-tauri/.sqlx/{query-962144198e9c20decf4c7aa7c83a5213873a3bd1a9c3c57be8487083f0778767.json => query-999106a981d0e33990bb03479da7d594750c967d6d30c3da7ce0e56fb089ea06.json} (54%) rename src-tauri/.sqlx/{query-8e76d29194fe47669a692494f9396e716aea42e5c0d214288dab8b9dbc9d8ce0.json => query-99e819c9e4d77ea568cb60f07e49ca8c407ab7b15ac505968c66924df3563ff8.json} (58%) rename src-tauri/.sqlx/{query-5c945afbf8a648173aaf1481ac44537b2ef3e3bfcaa2364c9c8524641934dab6.json => query-ba6d72e4ed26ebce7f1c1aa8147d81a7011ca163d2511fdebf95d8cb79c17d7a.json} (52%) rename src-tauri/.sqlx/{query-8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f.json => query-ccb326ab9b3ebd336c0fd9aed0da8e991f091f17e25583bd251347c207d8ff5c.json} (74%) rename src-tauri/.sqlx/{query-74235ba9425215a6064793d3b23f81f127b9851f6953cbb841ae5452f7cb5284.json => query-ce950c602d6620d98bc4c3518e2520e028bc2609b39578f1ccd5bfa9df2240e8.json} (75%) rename src-tauri/.sqlx/{query-8789e6ec4d91dccbba4c2c0ec9adc6ee40f101da31ee9e3e8a9642a6669a5d40.json => query-d364b350070c789f0d7e33fb1789dde7717aca865f082f24b500214814c4a3ef.json} (59%) create mode 100644 src-tauri/migrations/20231228102040_add_more_instance_info.sql diff --git a/src-tauri/.sqlx/query-b31daa7e35a53918352e0d3c4a5698368fd4a33bb5cafe5d2f680dc0ccf33f5a.json b/src-tauri/.sqlx/query-2519ee530137aff4f6d567cfd0732227ab0ed2df1a05397890de3a55d62d27ab.json similarity index 51% rename from src-tauri/.sqlx/query-b31daa7e35a53918352e0d3c4a5698368fd4a33bb5cafe5d2f680dc0ccf33f5a.json rename to src-tauri/.sqlx/query-2519ee530137aff4f6d567cfd0732227ab0ed2df1a05397890de3a55d62d27ab.json index 4bd2ca80..3084a476 100644 --- a/src-tauri/.sqlx/query-b31daa7e35a53918352e0d3c4a5698368fd4a33bb5cafe5d2f680dc0ccf33f5a.json +++ b/src-tauri/.sqlx/query-2519ee530137aff4f6d567cfd0732227ab0ed2df1a05397890de3a55d62d27ab.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO instance (name, uuid, url) VALUES ($1, $2, $3) RETURNING id;", + "query": "INSERT INTO instance (name, uuid, url, proxy_url, username) VALUES ($1, $2, $3, $4, $5) RETURNING id;", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 3 + "Right": 5 }, "nullable": [ false ] }, - "hash": "b31daa7e35a53918352e0d3c4a5698368fd4a33bb5cafe5d2f680dc0ccf33f5a" + "hash": "2519ee530137aff4f6d567cfd0732227ab0ed2df1a05397890de3a55d62d27ab" } diff --git a/src-tauri/.sqlx/query-8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd.json b/src-tauri/.sqlx/query-368814ed137d486c51e412c386ccd82ebaa96f1d438388521b4761da5bb93ce4.json similarity index 74% rename from src-tauri/.sqlx/query-8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd.json rename to src-tauri/.sqlx/query-368814ed137d486c51e412c386ccd82ebaa96f1d438388521b4761da5bb93ce4.json index fc136275..425a1789 100644 --- a/src-tauri/.sqlx/query-8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd.json +++ b/src-tauri/.sqlx/query-368814ed137d486c51e412c386ccd82ebaa96f1d438388521b4761da5bb93ce4.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic FROM location WHERE network_id = $1;", + "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, mfa_enabled, keepalive_interval FROM location WHERE instance_id = $1;", "describe": { "columns": [ { @@ -52,6 +52,16 @@ "name": "route_all_traffic", "ordinal": 9, "type_info": "Bool" + }, + { + "name": "mfa_enabled", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "keepalive_interval", + "ordinal": 11, + "type_info": "Int64" } ], "parameters": { @@ -67,8 +77,10 @@ false, true, false, + false, + false, false ] }, - "hash": "8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd" + "hash": "368814ed137d486c51e412c386ccd82ebaa96f1d438388521b4761da5bb93ce4" } diff --git a/src-tauri/.sqlx/query-68772513090d1bd3fbf58546971333d32a45eee6b9a70bb74713bee2f6283321.json b/src-tauri/.sqlx/query-68772513090d1bd3fbf58546971333d32a45eee6b9a70bb74713bee2f6283321.json new file mode 100644 index 00000000..60bd958c --- /dev/null +++ b/src-tauri/.sqlx/query-68772513090d1bd3fbf58546971333d32a45eee6b9a70bb74713bee2f6283321.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE instance SET name = $1, uuid = $2, url = $3, proxy_url = $4, username = $5 WHERE id = $6;", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "68772513090d1bd3fbf58546971333d32a45eee6b9a70bb74713bee2f6283321" +} diff --git a/src-tauri/.sqlx/query-807d90d55c57bf87c224a5d69357c90d16faf6c96ef650e6bc8d4e5e193e0855.json b/src-tauri/.sqlx/query-807d90d55c57bf87c224a5d69357c90d16faf6c96ef650e6bc8d4e5e193e0855.json deleted file mode 100644 index 13c05b80..00000000 --- a/src-tauri/.sqlx/query-807d90d55c57bf87c224a5d69357c90d16faf6c96ef650e6bc8d4e5e193e0855.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE instance SET name = $1, uuid = $2, url = $3 WHERE id = $4;", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "807d90d55c57bf87c224a5d69357c90d16faf6c96ef650e6bc8d4e5e193e0855" -} diff --git a/src-tauri/.sqlx/query-8c85b8c4cfd2b85ff96db3ce58ce727b3c77039d5984f523bb1923c8b81cbcd1.json b/src-tauri/.sqlx/query-89618232479b662d911715e0c9de427be3933d6ad26f685d1615fa2af0a09bc1.json similarity index 74% rename from src-tauri/.sqlx/query-8c85b8c4cfd2b85ff96db3ce58ce727b3c77039d5984f523bb1923c8b81cbcd1.json rename to src-tauri/.sqlx/query-89618232479b662d911715e0c9de427be3933d6ad26f685d1615fa2af0a09bc1.json index 347fb9b8..33471ed1 100644 --- a/src-tauri/.sqlx/query-8c85b8c4cfd2b85ff96db3ce58ce727b3c77039d5984f523bb1923c8b81cbcd1.json +++ b/src-tauri/.sqlx/query-89618232479b662d911715e0c9de427be3933d6ad26f685d1615fa2af0a09bc1.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic FROM location WHERE instance_id = $1;", + "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, mfa_enabled, keepalive_interval FROM location WHERE network_id = $1;", "describe": { "columns": [ { @@ -52,6 +52,16 @@ "name": "route_all_traffic", "ordinal": 9, "type_info": "Bool" + }, + { + "name": "mfa_enabled", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "keepalive_interval", + "ordinal": 11, + "type_info": "Int64" } ], "parameters": { @@ -67,8 +77,10 @@ false, true, false, + false, + false, false ] }, - "hash": "8c85b8c4cfd2b85ff96db3ce58ce727b3c77039d5984f523bb1923c8b81cbcd1" + "hash": "89618232479b662d911715e0c9de427be3933d6ad26f685d1615fa2af0a09bc1" } diff --git a/src-tauri/.sqlx/query-fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86.json b/src-tauri/.sqlx/query-8dfaa1c23af01a966442b52412eaa3a58f804f5d59d14fa70a65dba68c079b01.json similarity index 74% rename from src-tauri/.sqlx/query-fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86.json rename to src-tauri/.sqlx/query-8dfaa1c23af01a966442b52412eaa3a58f804f5d59d14fa70a65dba68c079b01.json index 9e297b50..beaa490d 100644 --- a/src-tauri/.sqlx/query-fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86.json +++ b/src-tauri/.sqlx/query-8dfaa1c23af01a966442b52412eaa3a58f804f5d59d14fa70a65dba68c079b01.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic FROM location WHERE pubkey = $1;", + "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, mfa_enabled, keepalive_interval FROM location WHERE pubkey = $1;", "describe": { "columns": [ { @@ -52,6 +52,16 @@ "name": "route_all_traffic", "ordinal": 9, "type_info": "Bool" + }, + { + "name": "mfa_enabled", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "keepalive_interval", + "ordinal": 11, + "type_info": "Int64" } ], "parameters": { @@ -67,8 +77,10 @@ false, true, false, + false, + false, false ] }, - "hash": "fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86" + "hash": "8dfaa1c23af01a966442b52412eaa3a58f804f5d59d14fa70a65dba68c079b01" } diff --git a/src-tauri/.sqlx/query-962144198e9c20decf4c7aa7c83a5213873a3bd1a9c3c57be8487083f0778767.json b/src-tauri/.sqlx/query-999106a981d0e33990bb03479da7d594750c967d6d30c3da7ce0e56fb089ea06.json similarity index 54% rename from src-tauri/.sqlx/query-962144198e9c20decf4c7aa7c83a5213873a3bd1a9c3c57be8487083f0778767.json rename to src-tauri/.sqlx/query-999106a981d0e33990bb03479da7d594750c967d6d30c3da7ce0e56fb089ea06.json index 57e6fde3..66f013e7 100644 --- a/src-tauri/.sqlx/query-962144198e9c20decf4c7aa7c83a5213873a3bd1a9c3c57be8487083f0778767.json +++ b/src-tauri/.sqlx/query-999106a981d0e33990bb03479da7d594750c967d6d30c3da7ce0e56fb089ea06.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9 WHERE id = $10;", + "query": "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9, mfa_enabled = $10, keepalive_interval = $11 WHERE id = $12;", "describe": { "columns": [], "parameters": { - "Right": 10 + "Right": 12 }, "nullable": [] }, - "hash": "962144198e9c20decf4c7aa7c83a5213873a3bd1a9c3c57be8487083f0778767" + "hash": "999106a981d0e33990bb03479da7d594750c967d6d30c3da7ce0e56fb089ea06" } diff --git a/src-tauri/.sqlx/query-8e76d29194fe47669a692494f9396e716aea42e5c0d214288dab8b9dbc9d8ce0.json b/src-tauri/.sqlx/query-99e819c9e4d77ea568cb60f07e49ca8c407ab7b15ac505968c66924df3563ff8.json similarity index 58% rename from src-tauri/.sqlx/query-8e76d29194fe47669a692494f9396e716aea42e5c0d214288dab8b9dbc9d8ce0.json rename to src-tauri/.sqlx/query-99e819c9e4d77ea568cb60f07e49ca8c407ab7b15ac505968c66924df3563ff8.json index 91b3aca6..e38d3947 100644 --- a/src-tauri/.sqlx/query-8e76d29194fe47669a692494f9396e716aea42e5c0d214288dab8b9dbc9d8ce0.json +++ b/src-tauri/.sqlx/query-99e819c9e4d77ea568cb60f07e49ca8c407ab7b15ac505968c66924df3563ff8.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id?\", name, uuid, url FROM instance WHERE id = $1;", + "query": "SELECT id \"id?\", name, uuid, url, proxy_url, username FROM instance WHERE id = $1;", "describe": { "columns": [ { @@ -22,17 +22,29 @@ "name": "url", "ordinal": 3, "type_info": "Text" + }, + { + "name": "proxy_url", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "username", + "ordinal": 5, + "type_info": "Text" } ], "parameters": { "Right": 1 }, "nullable": [ + false, + false, false, false, false, false ] }, - "hash": "8e76d29194fe47669a692494f9396e716aea42e5c0d214288dab8b9dbc9d8ce0" + "hash": "99e819c9e4d77ea568cb60f07e49ca8c407ab7b15ac505968c66924df3563ff8" } diff --git a/src-tauri/.sqlx/query-5c945afbf8a648173aaf1481ac44537b2ef3e3bfcaa2364c9c8524641934dab6.json b/src-tauri/.sqlx/query-ba6d72e4ed26ebce7f1c1aa8147d81a7011ca163d2511fdebf95d8cb79c17d7a.json similarity index 52% rename from src-tauri/.sqlx/query-5c945afbf8a648173aaf1481ac44537b2ef3e3bfcaa2364c9c8524641934dab6.json rename to src-tauri/.sqlx/query-ba6d72e4ed26ebce7f1c1aa8147d81a7011ca163d2511fdebf95d8cb79c17d7a.json index bad7bf8b..82ce3e98 100644 --- a/src-tauri/.sqlx/query-5c945afbf8a648173aaf1481ac44537b2ef3e3bfcaa2364c9c8524641934dab6.json +++ b/src-tauri/.sqlx/query-ba6d72e4ed26ebce7f1c1aa8147d81a7011ca163d2511fdebf95d8cb79c17d7a.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id;", + "query": "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, mfa_enabled, keepalive_interval) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id;", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 9 + "Right": 11 }, "nullable": [ false ] }, - "hash": "5c945afbf8a648173aaf1481ac44537b2ef3e3bfcaa2364c9c8524641934dab6" + "hash": "ba6d72e4ed26ebce7f1c1aa8147d81a7011ca163d2511fdebf95d8cb79c17d7a" } diff --git a/src-tauri/.sqlx/query-8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f.json b/src-tauri/.sqlx/query-ccb326ab9b3ebd336c0fd9aed0da8e991f091f17e25583bd251347c207d8ff5c.json similarity index 74% rename from src-tauri/.sqlx/query-8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f.json rename to src-tauri/.sqlx/query-ccb326ab9b3ebd336c0fd9aed0da8e991f091f17e25583bd251347c207d8ff5c.json index d628e720..c61be6e5 100644 --- a/src-tauri/.sqlx/query-8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f.json +++ b/src-tauri/.sqlx/query-ccb326ab9b3ebd336c0fd9aed0da8e991f091f17e25583bd251347c207d8ff5c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic FROM location WHERE id = $1;", + "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, mfa_enabled, keepalive_interval FROM location WHERE id = $1;", "describe": { "columns": [ { @@ -52,6 +52,16 @@ "name": "route_all_traffic", "ordinal": 9, "type_info": "Bool" + }, + { + "name": "mfa_enabled", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "keepalive_interval", + "ordinal": 11, + "type_info": "Int64" } ], "parameters": { @@ -67,8 +77,10 @@ false, true, false, + false, + false, false ] }, - "hash": "8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f" + "hash": "ccb326ab9b3ebd336c0fd9aed0da8e991f091f17e25583bd251347c207d8ff5c" } diff --git a/src-tauri/.sqlx/query-74235ba9425215a6064793d3b23f81f127b9851f6953cbb841ae5452f7cb5284.json b/src-tauri/.sqlx/query-ce950c602d6620d98bc4c3518e2520e028bc2609b39578f1ccd5bfa9df2240e8.json similarity index 75% rename from src-tauri/.sqlx/query-74235ba9425215a6064793d3b23f81f127b9851f6953cbb841ae5452f7cb5284.json rename to src-tauri/.sqlx/query-ce950c602d6620d98bc4c3518e2520e028bc2609b39578f1ccd5bfa9df2240e8.json index 9a56cde4..0b188d37 100644 --- a/src-tauri/.sqlx/query-74235ba9425215a6064793d3b23f81f127b9851f6953cbb841ae5452f7cb5284.json +++ b/src-tauri/.sqlx/query-ce950c602d6620d98bc4c3518e2520e028bc2609b39578f1ccd5bfa9df2240e8.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic FROM location;", + "query": "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,route_all_traffic, mfa_enabled, keepalive_interval FROM location;", "describe": { "columns": [ { @@ -52,6 +52,16 @@ "name": "route_all_traffic", "ordinal": 9, "type_info": "Bool" + }, + { + "name": "mfa_enabled", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "keepalive_interval", + "ordinal": 11, + "type_info": "Int64" } ], "parameters": { @@ -67,8 +77,10 @@ false, true, false, + false, + false, false ] }, - "hash": "74235ba9425215a6064793d3b23f81f127b9851f6953cbb841ae5452f7cb5284" + "hash": "ce950c602d6620d98bc4c3518e2520e028bc2609b39578f1ccd5bfa9df2240e8" } diff --git a/src-tauri/.sqlx/query-8789e6ec4d91dccbba4c2c0ec9adc6ee40f101da31ee9e3e8a9642a6669a5d40.json b/src-tauri/.sqlx/query-d364b350070c789f0d7e33fb1789dde7717aca865f082f24b500214814c4a3ef.json similarity index 59% rename from src-tauri/.sqlx/query-8789e6ec4d91dccbba4c2c0ec9adc6ee40f101da31ee9e3e8a9642a6669a5d40.json rename to src-tauri/.sqlx/query-d364b350070c789f0d7e33fb1789dde7717aca865f082f24b500214814c4a3ef.json index f1bcf1f8..0da2bc84 100644 --- a/src-tauri/.sqlx/query-8789e6ec4d91dccbba4c2c0ec9adc6ee40f101da31ee9e3e8a9642a6669a5d40.json +++ b/src-tauri/.sqlx/query-d364b350070c789f0d7e33fb1789dde7717aca865f082f24b500214814c4a3ef.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id?\", name, uuid, url FROM instance;", + "query": "SELECT id \"id?\", name, uuid, url, proxy_url, username FROM instance;", "describe": { "columns": [ { @@ -22,17 +22,29 @@ "name": "url", "ordinal": 3, "type_info": "Text" + }, + { + "name": "proxy_url", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "username", + "ordinal": 5, + "type_info": "Text" } ], "parameters": { "Right": 0 }, "nullable": [ + false, + false, false, false, false, false ] }, - "hash": "8789e6ec4d91dccbba4c2c0ec9adc6ee40f101da31ee9e3e8a9642a6669a5d40" + "hash": "d364b350070c789f0d7e33fb1789dde7717aca865f082f24b500214814c4a3ef" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8654aa00..6dbfea46 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", "getrandom 0.2.11", @@ -136,9 +136,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.76" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "arrayvec" @@ -163,7 +163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", - "event-listener 4.0.1", + "event-listener 4.0.2", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -249,7 +249,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" dependencies = [ - "event-listener 4.0.1", + "event-listener 4.0.2", "event-listener-strategy", "pin-project-lite", ] @@ -279,7 +279,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -319,7 +319,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -330,13 +330,13 @@ checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" [[package]] name = "async-trait" -version = "0.1.75" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -550,7 +550,7 @@ dependencies = [ "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", "syn_derive", ] @@ -577,9 +577,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" dependencies = [ "memchr", "serde", @@ -753,9 +753,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" dependencies = [ "clap_builder", "clap_derive", @@ -763,9 +763,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" dependencies = [ "anstream", "anstyle", @@ -782,7 +782,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -959,9 +959,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5" +checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" dependencies = [ "cfg-if", "crossbeam-utils", @@ -980,21 +980,20 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.16" +version = "0.9.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset 0.9.0", ] [[package]] name = "crossbeam-queue" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" +checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1002,9 +1001,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.17" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" dependencies = [ "cfg-if", ] @@ -1049,7 +1048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -1059,7 +1058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" dependencies = [ "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -1086,7 +1085,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -1127,7 +1126,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -1138,7 +1137,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -1220,9 +1219,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", @@ -1435,7 +1434,7 @@ checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -1484,9 +1483,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "4.0.1" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f2cdcf274580f2d63697192d744727b3198894b1bf02923643bf59e2c26712" +checksum = "218a870470cce1469024e9fb66b901aa983929d81304a1cdb299f28118e550d5" dependencies = [ "concurrent-queue", "parking", @@ -1499,7 +1498,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 4.0.1", + "event-listener 4.0.2", "pin-project-lite", ] @@ -1520,9 +1519,9 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fdeflate" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +checksum = "209098dd6dfc4445aa6111f0e98653ac323eaa4dfd212c9ca3931bf9955c31bd" dependencies = [ "simd-adler32", ] @@ -1654,9 +1653,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1664,15 +1663,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1692,9 +1691,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1726,32 +1725,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", @@ -2104,7 +2103,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.7", "allocator-api2", ] @@ -2279,9 +2278,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2774,9 +2773,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -3196,9 +3195,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -3211,9 +3210,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.61" +version = "0.10.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -3232,7 +3231,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -3252,9 +3251,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.97" +version = "0.9.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" dependencies = [ "cc", "libc", @@ -3494,7 +3493,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -3541,7 +3540,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -3596,9 +3595,9 @@ checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "platforms" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" [[package]] name = "plist" @@ -3677,12 +3676,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -3736,9 +3735,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" dependencies = [ "unicode-ident", ] @@ -3770,7 +3769,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.42", + "syn 2.0.46", "tempfile", "which", ] @@ -3785,7 +3784,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -3828,9 +3827,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -4197,11 +4196,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4276,29 +4275,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" dependencies = [ "itoa 1.0.10", "ryu", @@ -4307,13 +4306,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -4363,7 +4362,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -4584,7 +4583,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.7", "atoi", "byteorder", "bytes", @@ -4851,7 +4850,7 @@ checksum = "f14a349c27ebe59faba22f933c9c734d428da7231e88a247e9d8c61eea964ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -4873,7 +4872,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -4895,9 +4894,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.42" +version = "2.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" dependencies = [ "proc-macro2", "quote", @@ -4913,7 +4912,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -5162,7 +5161,7 @@ dependencies = [ [[package]] name = "tauri-plugin-log" version = "0.0.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#7de603ebff4c8900d1cc24644c5c5a2dd06ab48b" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#3ffa47c0e8ade8913a286eb8650fb51a85407ea0" dependencies = [ "byte-unit", "fern", @@ -5177,7 +5176,7 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" version = "0.0.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#7de603ebff4c8900d1cc24644c5c5a2dd06ab48b" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#3ffa47c0e8ade8913a286eb8650fb51a85407ea0" dependencies = [ "log", "serde", @@ -5271,15 +5270,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall", "rustix 0.38.28", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5301,22 +5300,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -5421,7 +5420,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -5575,7 +5574,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -5642,7 +5641,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -5921,7 +5920,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", "wasm-bindgen-shared", ] @@ -5955,7 +5954,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6158,11 +6157,11 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -6360,9 +6359,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.30" +version = "0.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" +checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" dependencies = [ "memchr", ] @@ -6478,9 +6477,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dae5072fe1f8db8f8d29059189ac175196e410e40ba42d5d4684ae2f750995" +checksum = "914566e6413e7fa959cc394fb30e563ba80f3541fbd40816d4c05a0fc3f2a0f1" dependencies = [ "libc", "linux-raw-sys 0.4.12", @@ -6565,22 +6564,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] @@ -6600,7 +6599,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.42", + "syn 2.0.46", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 480d655d..6caad1f6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,37 +19,37 @@ anyhow = "1.0" base64 = "0.21" clap = { version = "4.4", features = ["derive", "env"] } chrono = { version = "0.4", features = ["serde"] } +dark-light = "1.0" defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", branch = "main" } dirs = "5.0" +lazy_static = "1.4" local-ip-address = "0.5" log = "0.4" notify-debouncer-mini = "0.4" prost = "0.12" rand = "0.8" +rust-ini = "0.20" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } serde_with = "3.4" sqlx = { version = "0.7", features = ["chrono", "sqlite", "runtime-tokio", "uuid", "macros"] } -thiserror = "1.0" -tonic = "0.10" -x25519-dalek = { version = "2", features = [ - "getrandom", - "static_secrets", -] } +struct-patch = "0.4" strum = { version = "0.25", features = ["derive"] } -dark-light = "1.0" - -tauri = { version = "1.5", features = [ "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png", "fs-all"] } +tauri = { version = "1.5", features = [ "fs-all", "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } +thiserror = "1.0" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tokio-util = "0.7" +tonic = "0.10" tracing = "0.1" tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } -lazy_static = "1.4" -rust-ini = "0.20" -struct-patch = "0.4" +x25519-dalek = { version = "2", features = [ + "getrandom", + "static_secrets", +] } + [target.'cfg(target_os = "macos")'.dependencies] nix = { version = "0.27", features = ["net"] } diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 1950c580..6e453e64 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -7,8 +7,11 @@ fn main() -> Result<(), Box> { config.type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]"); tonic_build::configure().compile_with_config( config, - &["proto/client/client.proto"], - &["proto/client"], + &[ + "proto/client/client.proto", + "proto/enrollment/enrollment.proto", + ], + &["proto/client", "proto/enrollment"], )?; tauri_build::build(); diff --git a/src-tauri/migrations/20231228102040_add_more_instance_info.sql b/src-tauri/migrations/20231228102040_add_more_instance_info.sql new file mode 100644 index 00000000..5f324612 --- /dev/null +++ b/src-tauri/migrations/20231228102040_add_more_instance_info.sql @@ -0,0 +1,7 @@ +-- update instance table +ALTER TABLE instance ADD COLUMN proxy_url TEXT NOT NULL; +ALTER TABLE instance ADD COLUMN username TEXT NOT NULL; + +-- update location table +ALTER TABLE location ADD COLUMN mfa_enabled BOOLEAN NOT NULL; +ALTER TABLE location ADD COLUMN keepalive_interval INTEGER NOT NULL; diff --git a/src-tauri/proto b/src-tauri/proto index 50f3791a..9f5c9026 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit 50f3791a2d0104ad7d9ae69a3abde070743902c2 +Subproject commit 9f5c90266c9d3449c38197b72e8a9d8a56f12cfd diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index 7b41ed76..d2affb5f 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -43,11 +43,21 @@ impl AppState { } pub fn get_pool(&self) -> DbPool { - self.db.lock().unwrap().as_ref().cloned().unwrap() + self.db + .lock() + .expect("Failed to lock dbpool mutex") + .as_ref() + .cloned() + .unwrap() } + pub fn get_connections(&self) -> Vec { - self.active_connections.lock().unwrap().clone() + self.active_connections + .lock() + .expect("Failed to lock active connections mutex") + .clone() } + pub fn find_and_remove_connection(&self, location_id: i64) -> Option { debug!("Removing active connection for location with id: {location_id}"); let mut connections = self.active_connections.lock().unwrap(); @@ -89,6 +99,7 @@ impl AppState { } Ok(()) } + pub fn find_connection(&self, location_id: i64) -> Option { let connections = self.active_connections.lock().unwrap(); debug!("Checking for active connection with location id: {location_id} in active connections: {connections:#?}"); diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 5f0343eb..026cfbe4 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -74,7 +74,7 @@ async fn main() { LevelFilter::from_str(&env::var("DEFGUARD_CLIENT_LOG_LEVEL").unwrap_or("info".into())) .unwrap_or(LevelFilter::Info); - tauri::Builder::default() + let app = tauri::Builder::default() .invoke_handler(tauri::generate_handler![ all_locations, save_device_config, @@ -129,31 +129,30 @@ async fn main() { .build(), ) .manage(AppState::default()) - .setup(|app| { - let handle = app.handle(); - tauri::async_runtime::spawn(async move { - debug!("Initializing database connection"); - let app_state: State = handle.state(); - let db = database::init_db(&handle) - .await - .expect("Database initialization failed"); - *app_state.db.lock().unwrap() = Some(db); - info!("Database initialization completed"); - info!("Starting main app thread."); - let result = database::info(&app_state.get_pool()).await; - info!("Database info result: {:#?}", result); - // configure tray - if let Ok(settings) = Settings::get(&app_state.get_pool()).await { - configure_tray_icon(&handle, &settings.tray_icon_theme).unwrap(); - } - }); - Ok(()) - }) .build(tauri::generate_context!()) - .expect("error while running tauri application") - .run(|_app_handle, event| { - if let tauri::RunEvent::ExitRequested { api, .. } = event { - api.prevent_exit(); - } - }); + .expect("error while running tauri application"); + + // initialize database + let app_handle = app.handle(); + debug!("Initializing database connection"); + let app_state: State = app_handle.state(); + let db = database::init_db(&app_handle) + .await + .expect("Database initialization failed"); + *app_state.db.lock().unwrap() = Some(db); + info!("Database initialization completed"); + info!("Starting main app thread."); + let result = database::info(&app_state.get_pool()).await; + info!("Database info result: {:#?}", result); + // configure tray + if let Ok(settings) = Settings::get(&app_state.get_pool()).await { + configure_tray_icon(&app_handle, &settings.tray_icon_theme).unwrap(); + } + + // run app + app.run(|_app_handle, event| { + if let tauri::RunEvent::ExitRequested { api, .. } = event { + api.prevent_exit(); + } + }); } diff --git a/src-tauri/src/bin/defguard-service.rs b/src-tauri/src/bin/defguard-service.rs index f3cf93c4..d5398b6d 100644 --- a/src-tauri/src/bin/defguard-service.rs +++ b/src-tauri/src/bin/defguard-service.rs @@ -25,17 +25,21 @@ async fn main() -> anyhow::Result<()> { let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); // prepare log level filter for stdout - let filter = EnvFilter::try_from_default_env() + let stdout_filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| format!("{},hyper=info", config.log_level).into()); + // prepare log level filter for json file + let json_filter = EnvFilter::new(format!("{},hyper=info", tracing::Level::DEBUG)); + // prepare tracing layers let stdout_layer = fmt::layer() .pretty() .with_writer(stdout.with_max_level(tracing::Level::DEBUG)) - .with_filter(filter); + .with_filter(stdout_filter); let json_file_layer = fmt::layer() .json() - .with_writer(non_blocking.with_max_level(tracing::Level::DEBUG)); + .with_writer(non_blocking.with_max_level(tracing::Level::DEBUG)) + .with_filter(json_filter); // initialize tracing subscriber tracing_subscriber::registry() diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index a73724fb..d870a5e1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -6,6 +6,7 @@ use crate::{ Tunnel, WireguardKeys, }, error::Error, + proto::{DeviceConfig, DeviceConfigResponse}, service::{ log_watcher::{spawn_log_watcher_task, stop_log_watcher_task}, proto::RemoveInterfaceRequest, @@ -131,18 +132,6 @@ pub struct Device { pub created_at: i64, } -#[derive(Serialize, Deserialize, Debug)] -pub struct DeviceConfig { - pub network_id: i64, - pub network_name: String, - pub config: String, - pub endpoint: String, - pub assigned_ip: String, - pub pubkey: String, - pub allowed_ips: String, - pub dns: Option, -} - #[must_use] pub fn device_config_to_location(device_config: DeviceConfig, instance_id: i64) -> Location { Location { @@ -156,6 +145,8 @@ pub fn device_config_to_location(device_config: DeviceConfig, instance_id: i64) allowed_ips: device_config.allowed_ips, dns: device_config.dns, route_all_traffic: false, + mfa_enabled: device_config.mfa_enabled, + keepalive_interval: device_config.keepalive_interval.into(), } } #[derive(Serialize, Deserialize, Debug)] @@ -166,13 +157,6 @@ pub struct InstanceResponse { pub url: String, } -#[derive(Serialize, Deserialize, Debug)] -pub struct CreateDeviceResponse { - instance: InstanceResponse, - configs: Vec, - device: Device, -} - #[derive(Serialize, Deserialize, Debug)] pub struct SaveDeviceConfigResponse { locations: Vec, @@ -182,24 +166,26 @@ pub struct SaveDeviceConfigResponse { #[tauri::command(async)] pub async fn save_device_config( private_key: String, - response: CreateDeviceResponse, + response: DeviceConfigResponse, app_state: State<'_, AppState>, handle: AppHandle, ) -> Result { debug!("Received device configuration: {response:#?}"); let mut transaction = app_state.get_pool().begin().await?; - let mut instance = Instance::new( - response.instance.name, - response.instance.id, - response.instance.url, - ); + let instance_info = response + .instance + .expect("Missing instance info in device config response"); + let mut instance: Instance = instance_info.into(); instance.save(&mut *transaction).await?; + let device = response + .device + .expect("Missing device info in device config response"); let mut keys = WireguardKeys::new( instance.id.expect("Missing instance ID"), - response.device.pubkey, + device.pubkey, private_key, ); keys.save(&mut *transaction).await?; @@ -240,7 +226,7 @@ pub async fn all_instances(app_state: State<'_, AppState>) -> Result) -> Result, app_handle: AppHandle, ) -> Result<(), Error> { @@ -406,8 +393,13 @@ pub async fn update_instance( let instance = Instance::find_by_id(&app_state.get_pool(), instance_id).await?; if let Some(mut instance) = instance { let mut transaction = app_state.get_pool().begin().await?; - instance.name = response.instance.name; - instance.url = response.instance.url; + let instance_info = response + .instance + .expect("Missing instance info in device config response"); + instance.name = instance_info.name; + instance.url = instance_info.url; + instance.proxy_url = instance_info.proxy_url; + instance.username = instance_info.username; instance.save(&mut *transaction).await?; for location in response.configs { @@ -420,6 +412,8 @@ pub async fn update_instance( old_location.pubkey = new_location.pubkey; old_location.endpoint = new_location.endpoint; old_location.allowed_ips = new_location.allowed_ips; + old_location.mfa_enabled = new_location.mfa_enabled; + old_location.keepalive_interval = new_location.keepalive_interval; old_location.save(&mut *transaction).await?; } else { new_location.save(&mut *transaction).await?; diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 89dcf4fe..d614b638 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -38,7 +38,11 @@ pub async fn init_db(app_handle: &AppHandle) -> Result { ); } debug!("Connecting to database: {}", db_path.to_string_lossy()); - let pool = DbPool::connect(&format!("sqlite://{}", db_path.to_str().unwrap())).await?; + let pool = DbPool::connect(&format!( + "sqlite://{}", + db_path.to_str().expect("Failed to format DB path") + )) + .await?; debug!("Running migrations."); sqlx::migrate!().run(&pool).await?; Settings::init_defaults(&pool).await?; diff --git a/src-tauri/src/database/models/instance.rs b/src-tauri/src/database/models/instance.rs index b162ee92..108f487c 100644 --- a/src-tauri/src/database/models/instance.rs +++ b/src-tauri/src/database/models/instance.rs @@ -1,4 +1,4 @@ -use crate::{database::DbPool, error::Error}; +use crate::{database::DbPool, error::Error, proto}; use serde::{Deserialize, Serialize}; use sqlx::{query, query_as, FromRow}; @@ -8,16 +8,39 @@ pub struct Instance { pub name: String, pub uuid: String, pub url: String, + pub proxy_url: String, + pub username: String, +} + +impl From for Instance { + fn from(instance_info: proto::InstanceInfo) -> Self { + Self { + id: None, + name: instance_info.name, + uuid: instance_info.id, + url: instance_info.url, + proxy_url: instance_info.proxy_url, + username: instance_info.username, + } + } } impl Instance { #[must_use] - pub fn new(name: String, uuid: String, url: String) -> Self { + pub fn new( + name: String, + uuid: String, + url: String, + proxy_url: String, + username: String, + ) -> Self { Instance { id: None, name, uuid, url, + proxy_url, + username, } } @@ -25,13 +48,17 @@ impl Instance { where E: sqlx::Executor<'e, Database = sqlx::Sqlite>, { + let url = self.url.to_string(); + let proxy_url = self.proxy_url.to_string(); match self.id { None => { let result = query!( - "INSERT INTO instance (name, uuid, url) VALUES ($1, $2, $3) RETURNING id;", + "INSERT INTO instance (name, uuid, url, proxy_url, username) VALUES ($1, $2, $3, $4, $5) RETURNING id;", self.name, self.uuid, - self.url + url, + proxy_url, + self.username, ) .fetch_one(executor) .await?; @@ -41,10 +68,12 @@ impl Instance { Some(id) => { // Update the existing record when there is an ID query!( - "UPDATE instance SET name = $1, uuid = $2, url = $3 WHERE id = $4;", + "UPDATE instance SET name = $1, uuid = $2, url = $3, proxy_url = $4, username = $5 WHERE id = $6;", self.name, self.uuid, - self.url, + url, + proxy_url, + self.username, id ) .execute(executor) @@ -55,16 +84,19 @@ impl Instance { } pub async fn all(pool: &DbPool) -> Result, Error> { - let instances = query_as!(Self, "SELECT id \"id?\", name, uuid, url FROM instance;") - .fetch_all(pool) - .await?; + let instances = query_as!( + Self, + "SELECT id \"id?\", name, uuid, url, proxy_url, username FROM instance;" + ) + .fetch_all(pool) + .await?; Ok(instances) } pub async fn find_by_id(pool: &DbPool, id: i64) -> Result, Error> { let instance = query_as!( Self, - "SELECT id \"id?\", name, uuid, url FROM instance WHERE id = $1;", + "SELECT id \"id?\", name, uuid, url, proxy_url, username FROM instance WHERE id = $1;", id ) .fetch_optional(pool) @@ -97,6 +129,7 @@ pub struct InstanceInfo { pub name: String, pub uuid: String, pub url: String, + pub proxy_url: String, pub active: bool, pub pubkey: String, } diff --git a/src-tauri/src/database/models/location.rs b/src-tauri/src/database/models/location.rs index 0e8ae840..0d4ae0d9 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -19,6 +19,8 @@ pub struct Location { pub allowed_ips: String, pub dns: Option, pub route_all_traffic: bool, + pub mfa_enabled: bool, + pub keepalive_interval: i64, } #[derive(FromRow, Debug, Serialize, Deserialize)] @@ -55,36 +57,11 @@ pub async fn peer_to_location_stats( } impl Location { - #[allow(clippy::too_many_arguments)] - #[must_use] - pub fn new( - instance_id: i64, - network_id: i64, - name: String, - address: String, - pubkey: String, - endpoint: String, - allowed_ips: String, - dns: Option, - ) -> Self { - Location { - id: None, - instance_id, - network_id, - name, - address, - pubkey, - endpoint, - allowed_ips, - dns, - route_all_traffic: false, - } - } - pub async fn all(pool: &DbPool) -> Result, Error> { let locations = query_as!( Self, - "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic \ + "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,\ + route_all_traffic, mfa_enabled, keepalive_interval \ FROM location;" ) .fetch_all(pool) @@ -100,18 +77,20 @@ impl Location { None => { // Insert a new record when there is no ID let result = query!( - "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic) \ - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) \ - RETURNING id;", - self.instance_id, - self.name, - self.address, - self.pubkey, - self.endpoint, - self.allowed_ips, - self.dns, - self.network_id, - self.route_all_traffic, + "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, mfa_enabled, keepalive_interval) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) \ + RETURNING id;", + self.instance_id, + self.name, + self.address, + self.pubkey, + self.endpoint, + self.allowed_ips, + self.dns, + self.network_id, + self.route_all_traffic, + self.mfa_enabled, + self.keepalive_interval ) .fetch_one(executor) .await?; @@ -120,18 +99,20 @@ impl Location { Some(id) => { // Update the existing record when there is an ID query!( - "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, endpoint = $5, allowed_ips = $6, dns = $7, \ - network_id = $8, route_all_traffic = $9 WHERE id = $10;", - self.instance_id, - self.name, - self.address, - self.pubkey, - self.endpoint, - self.allowed_ips, - self.dns, - self.network_id, - self.route_all_traffic, - id, + "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, endpoint = $5, allowed_ips = $6, dns = $7, \ + network_id = $8, route_all_traffic = $9, mfa_enabled = $10, keepalive_interval = $11 WHERE id = $12;", + self.instance_id, + self.name, + self.address, + self.pubkey, + self.endpoint, + self.allowed_ips, + self.dns, + self.network_id, + self.route_all_traffic, + self.mfa_enabled, + self.keepalive_interval, + id, ) .execute(executor) .await?; @@ -144,7 +125,8 @@ impl Location { pub async fn find_by_id(pool: &DbPool, location_id: i64) -> Result, SqlxError> { query_as!( Self, - "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic \ + "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, \ + route_all_traffic, mfa_enabled, keepalive_interval \ FROM location WHERE id = $1;", location_id ) @@ -158,7 +140,8 @@ impl Location { ) -> Result, SqlxError> { query_as!( Self, - "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic \ + "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, \ + route_all_traffic, mfa_enabled, keepalive_interval \ FROM location WHERE instance_id = $1;", instance_id ) @@ -169,7 +152,8 @@ impl Location { pub async fn find_by_public_key(pool: &DbPool, pubkey: &str) -> Result { query_as!( Self, - "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic \ + "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, \ + route_all_traffic, mfa_enabled, keepalive_interval \ FROM location WHERE pubkey = $1;", pubkey ) @@ -186,7 +170,8 @@ impl Location { { query_as!( Self, - "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic \ + "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, \ + route_all_traffic, mfa_enabled, keepalive_interval \ FROM location WHERE network_id = $1;", instance_id ) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index caebda14..11b95f73 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,10 @@ pub mod tray; pub mod utils; pub mod wg_config; +pub mod proto { + tonic::include_proto!("enrollment"); +} + #[derive(Clone, serde::Serialize)] struct Payload { args: Vec, From 67d97657e72cadcfbf06a31faa69f0b57cb96f7b Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Wed, 3 Jan 2024 16:13:07 +0100 Subject: [PATCH 17/45] feat: Tunnel details (#141) * Tunnel detail page and connect/disconnect --- ...34948a042acdb8363f7735642da642420bfe6.json | 56 +++ ...796e6944d87b1be160ce7aa2cdbf57c20e78.json} | 4 +- ...81479f26da26e445a636d7cf6690a9d7a720f.json | 32 ++ src-tauri/src/appstate.rs | 51 ++- src-tauri/src/commands.rs | 354 ++++++++-------- src-tauri/src/database/mod.rs | 2 +- src-tauri/src/database/models/connection.rs | 39 +- src-tauri/src/database/models/location.rs | 21 +- src-tauri/src/database/models/tunnel.rs | 129 +++++- src-tauri/src/lib.rs | 55 +++ src-tauri/src/service/log_watcher.rs | 12 +- src-tauri/src/utils.rs | 380 +++++++++++++++++- src/components/App/App.tsx | 5 - src/pages/client/ClientPage.tsx | 27 +- src/pages/client/clientAPI/types.ts | 6 +- .../ClientSideBar/ClientSideBar.tsx | 2 +- .../ClientBarItem/ClientBarItem.tsx | 14 +- src/pages/client/hooks/useClientStore.tsx | 11 + .../ClientInstancePage/ClientInstancePage.tsx | 39 +- .../LocationsList/LocationsList.tsx | 35 +- .../LocationCardConnectButton.tsx | 6 +- .../LocationCardInfo/LocationCardInfo.tsx | 18 +- .../LocationCardRoute/LocationCardRoute.tsx | 3 +- .../LocationsDetailView.tsx | 71 +++- .../LocationConnectionHistory.tsx | 11 +- .../LocationDetailCard/LocationDetailCard.tsx | 16 +- .../LocationDetails/LocationDetails.tsx | 15 +- .../components/LocationLogs/LocationLogs.tsx | 7 +- .../LocationsGridView/LocationsGridView.tsx | 22 +- .../ClientTunnelPage/ClientTunnelPage.tsx | 36 -- .../client/pages/ClientTunnelPage/style.scss | 0 src/pages/client/types.ts | 3 +- src/shared/routes.ts | 1 - 33 files changed, 1154 insertions(+), 329 deletions(-) create mode 100644 src-tauri/.sqlx/query-3fe0268bce98cbce7cbf4ce8a4134948a042acdb8363f7735642da642420bfe6.json rename src-tauri/.sqlx/{query-ae23a30884275531d64ecc60c21b9eafef31be52e22b9d0c22ad565f0cdddce9.json => query-c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78.json} (94%) create mode 100644 src-tauri/.sqlx/query-ea81679acb33913c851077e7a2681479f26da26e445a636d7cf6690a9d7a720f.json delete mode 100644 src/pages/client/pages/ClientTunnelPage/ClientTunnelPage.tsx delete mode 100644 src/pages/client/pages/ClientTunnelPage/style.scss diff --git a/src-tauri/.sqlx/query-3fe0268bce98cbce7cbf4ce8a4134948a042acdb8363f7735642da642420bfe6.json b/src-tauri/.sqlx/query-3fe0268bce98cbce7cbf4ce8a4134948a042acdb8363f7735642da642420bfe6.json new file mode 100644 index 00000000..9946a3a1 --- /dev/null +++ b/src-tauri/.sqlx/query-3fe0268bce98cbce7cbf4ce8a4134948a042acdb8363f7735642da642420bfe6.json @@ -0,0 +1,56 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n c.id as \"id!\",\n c.tunnel_id as \"tunnel_id!\",\n c.connected_from as \"connected_from!\",\n c.start as \"start!\",\n c.end as \"end!\",\n COALESCE((\n SELECT ls.upload\n FROM tunnel_stats AS ls\n WHERE ls.tunnel_id = c.tunnel_id\n AND ls.collected_at >= c.start\n AND ls.collected_at <= c.end\n ORDER BY ls.collected_at DESC\n LIMIT 1\n ), 0) as \"upload: _\",\n COALESCE((\n SELECT ls.download\n FROM tunnel_stats AS ls\n WHERE ls.tunnel_id = c.tunnel_id\n AND ls.collected_at >= c.start\n AND ls.collected_at <= c.end\n ORDER BY ls.collected_at DESC\n LIMIT 1\n ), 0) as \"download: _\"\n FROM tunnel_connection AS c WHERE tunnel_id = $1\n ORDER BY start DESC;\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "tunnel_id!", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "connected_from!", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "start!", + "ordinal": 3, + "type_info": "Datetime" + }, + { + "name": "end!", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "upload: _", + "ordinal": 5, + "type_info": "Null" + }, + { + "name": "download: _", + "ordinal": 6, + "type_info": "Null" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false, + false, + false, + false, + null, + null + ] + }, + "hash": "3fe0268bce98cbce7cbf4ce8a4134948a042acdb8363f7735642da642420bfe6" +} diff --git a/src-tauri/.sqlx/query-ae23a30884275531d64ecc60c21b9eafef31be52e22b9d0c22ad565f0cdddce9.json b/src-tauri/.sqlx/query-c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78.json similarity index 94% rename from src-tauri/.sqlx/query-ae23a30884275531d64ecc60c21b9eafef31be52e22b9d0c22ad565f0cdddce9.json rename to src-tauri/.sqlx/query-c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78.json index af2992a8..5d303f03 100644 --- a/src-tauri/.sqlx/query-ae23a30884275531d64ecc60c21b9eafef31be52e22b9d0c22ad565f0cdddce9.json +++ b/src-tauri/.sqlx/query-c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id?\", name, pubkey, prvkey, address, server_pubkey, allowed_ips, endpoint, dns, persistent_keep_alive, \n route_all_traffic, pre_up, post_up, pre_down, post_down FROM tunnel WHERE pubkey = $1;", + "query": "SELECT id \"id?\", name, pubkey, prvkey, address, server_pubkey, allowed_ips, endpoint, dns, persistent_keep_alive, \n route_all_traffic, pre_up, post_up, pre_down, post_down FROM tunnel WHERE server_pubkey = $1;", "describe": { "columns": [ { @@ -100,5 +100,5 @@ true ] }, - "hash": "ae23a30884275531d64ecc60c21b9eafef31be52e22b9d0c22ad565f0cdddce9" + "hash": "c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78" } diff --git a/src-tauri/.sqlx/query-ea81679acb33913c851077e7a2681479f26da26e445a636d7cf6690a9d7a720f.json b/src-tauri/.sqlx/query-ea81679acb33913c851077e7a2681479f26da26e445a636d7cf6690a9d7a720f.json new file mode 100644 index 00000000..1000983d --- /dev/null +++ b/src-tauri/.sqlx/query-ea81679acb33913c851077e7a2681479f26da26e445a636d7cf6690a9d7a720f.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT last_handshake, listen_port as \"listen_port!: u32\",\n persistent_keepalive_interval as \"persistent_keepalive_interval?: u16\"\n FROM tunnel_stats\n WHERE tunnel_id = $1 ORDER BY collected_at DESC LIMIT 1\n ", + "describe": { + "columns": [ + { + "name": "last_handshake", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "listen_port!: u32", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "persistent_keepalive_interval?: u16", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "ea81679acb33913c851077e7a2681479f26da26e445a636d7cf6690a9d7a720f" +} diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index d2affb5f..89a36dd5 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -15,6 +15,7 @@ use crate::{ }, utils::setup_client, }, + ConnectionType, }; pub struct AppState { @@ -57,24 +58,43 @@ impl AppState { .expect("Failed to lock active connections mutex") .clone() } - - pub fn find_and_remove_connection(&self, location_id: i64) -> Option { + pub fn find_and_remove_connection( + &self, + location_id: i64, + connection_type: &ConnectionType, + ) -> Option { debug!("Removing active connection for location with id: {location_id}"); let mut connections = self.active_connections.lock().unwrap(); - if let Some(index) = connections - .iter() - .position(|conn| conn.location_id == location_id) - { + if let Some(index) = connections.iter().position(|conn| { + conn.location_id == location_id && conn.connection_type.eq(connection_type) + }) { // Found a connection with the specified location_id let removed_connection = connections.remove(index); info!("Removed connection from active connections: {removed_connection:#?}"); Some(removed_connection) } else { - None // Connection not found + None } } + pub fn get_connection_id_by_type(&self, connection_type: &ConnectionType) -> Vec { + let active_connections = self.active_connections.lock().unwrap(); + + let connection_ids: Vec = active_connections + .iter() + .filter_map(|con| { + if con.connection_type.eq(connection_type) { + Some(con.location_id) + } else { + None + } + }) + .collect(); + + connection_ids + } + pub async fn close_all_connections(&self) -> Result<(), crate::error::Error> { for connection in self.get_connections() { debug!("Found active connection"); @@ -99,20 +119,25 @@ impl AppState { } Ok(()) } - - pub fn find_connection(&self, location_id: i64) -> Option { + pub fn find_connection( + &self, + id: i64, + connection_type: ConnectionType, + ) -> Option { let connections = self.active_connections.lock().unwrap(); - debug!("Checking for active connection with location id: {location_id} in active connections: {connections:#?}"); + debug!( + "Checking for active connection with id: {id}, connection_type: {connection_type:?} in active connections: {connections:#?}" + ); if let Some(connection) = connections .iter() - .find(|conn| conn.location_id == location_id) + .find(|conn| conn.location_id == id && conn.connection_type == connection_type) { - // 'connection' now contains the first element with the specified location_id + // 'connection' now contains the first element with the specified id and connection_type debug!("Found connection: {connection:#?}"); Some(connection.to_owned()) } else { - error!("Element with location_id {location_id} not found."); + error!("Element with id: {id}, connection_type: {connection_type:?} not found."); None } } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d870a5e1..1bd8860d 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -3,91 +3,64 @@ use crate::{ database::{ models::{instance::InstanceInfo, settings::SettingsPatch}, ActiveConnection, Connection, ConnectionInfo, Instance, Location, LocationStats, Settings, - Tunnel, WireguardKeys, + Tunnel, TunnelConnection, TunnelConnectionInfo, TunnelStats, WireguardKeys, }, error::Error, proto::{DeviceConfig, DeviceConfigResponse}, - service::{ - log_watcher::{spawn_log_watcher_task, stop_log_watcher_task}, - proto::RemoveInterfaceRequest, - }, + service::{log_watcher::stop_log_watcher_task, proto::RemoveInterfaceRequest}, tray::configure_tray_icon, - utils::{get_interface_name, setup_interface, spawn_stats_thread}, + utils::{ + get_location_interface_details, get_tunnel_interface_details, + handle_connection_for_location, handle_connection_for_tunnel, + }, wg_config::parse_wireguard_config, + CommonConnection, CommonConnectionInfo, CommonLocationStats, ConnectionType, }; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; -use local_ip_address::local_ip; use serde::{Deserialize, Serialize}; -use sqlx::query; use std::str::FromStr; use struct_patch::Patch; use tauri::{AppHandle, Manager, State}; -use tracing::Level; #[derive(Clone, serde::Serialize)] -struct Payload { - message: String, +pub struct Payload { + pub message: String, } // Create new WireGuard interface #[tauri::command(async)] -pub async fn connect(location_id: i64, handle: AppHandle) -> Result<(), Error> { +pub async fn connect( + location_id: i64, + connection_type: ConnectionType, + handle: AppHandle, +) -> Result<(), Error> { let state = handle.state::(); - if let Some(location) = Location::find_by_id(&state.get_pool(), location_id).await? { - debug!( - "Creating new interface connection for location: {}", - location.name - ); - #[cfg(target_os = "macos")] - let interface_name = get_interface_name(); - #[cfg(not(target_os = "macos"))] - let interface_name = get_interface_name(&location); - setup_interface( - &location, - interface_name.clone(), - &state.get_pool(), - state.client.clone(), - ) - .await?; - let address = local_ip()?; - let connection = - ActiveConnection::new(location_id, address.to_string(), interface_name.clone()); - state - .active_connections - .lock() - .map_err(|_| Error::MutexError)? - .push(connection); - debug!( - "Active connections: {:#?}", - state - .active_connections - .lock() - .map_err(|_| Error::MutexError)? - ); - debug!("Sending event connection-changed."); - handle.emit_all( - "connection-changed", - Payload { - message: "Created new connection".into(), - }, - )?; - - // Spawn stats threads - debug!("Spawning stats thread"); - spawn_stats_thread(handle.clone(), interface_name.clone()).await; - - // spawn log watcher - spawn_log_watcher_task(handle, location_id, interface_name, Level::DEBUG, None).await?; + if connection_type.eq(&ConnectionType::Location) { + if let Some(location) = Location::find_by_id(&state.get_pool(), location_id).await? { + handle_connection_for_location(&location, handle).await? + } else { + error!("Location {location_id} not found"); + return Err(Error::NotFound); + } + } else if let Some(tunnel) = Tunnel::find_by_id(&state.get_pool(), location_id).await? { + handle_connection_for_tunnel(&tunnel, handle).await? + } else { + error!("Tunnel {location_id} not found"); + return Err(Error::NotFound); } Ok(()) } #[tauri::command] -pub async fn disconnect(location_id: i64, handle: AppHandle) -> Result<(), Error> { +pub async fn disconnect( + location_id: i64, + connection_type: ConnectionType, + handle: AppHandle, +) -> Result<(), Error> { debug!("Disconnecting location {}", location_id); let state = handle.state::(); - if let Some(connection) = state.find_and_remove_connection(location_id) { + if let Some(connection) = state.find_and_remove_connection(location_id, &connection_type) { debug!("Found active connection"); trace!("Connection: {:#?}", connection); debug!("Removing interface"); @@ -103,10 +76,16 @@ pub async fn disconnect(location_id: i64, handle: AppHandle) -> Result<(), Error debug!("Removed interface"); debug!("Saving connection"); trace!("Connection: {:#?}", connection); - let mut connection: Connection = connection.into(); - connection.save(&state.get_pool()).await?; + if connection_type.eq(&ConnectionType::Location) { + let mut connection: Connection = connection.into(); + connection.save(&state.get_pool()).await?; + trace!("Saved connection: {connection:#?}"); + } else { + let mut connection: TunnelConnection = connection.into(); + connection.save(&state.get_pool()).await?; + trace!("Saved connection: {connection:#?}"); + } debug!("Connection saved"); - trace!("Saved connection: {connection:#?}"); handle.emit_all( "connection-changed", Payload { @@ -116,7 +95,7 @@ pub async fn disconnect(location_id: i64, handle: AppHandle) -> Result<(), Error stop_log_watcher_task(handle, interface_name)?; - info!("Location {} disconnected", connection.location_id); + info!("Location {location_id} {connection_type:?} disconnected"); Ok(()) } else { error!("Connection for location with id: {location_id} not found"); @@ -219,13 +198,7 @@ pub async fn all_instances(app_state: State<'_, AppState>) -> Result = vec![]; - let connection_ids: Vec = app_state - .active_connections - .lock() - .map_err(|_| Error::MutexError)? - .iter() - .map(|connection| connection.location_id) - .collect(); + let connection_ids: Vec = app_state.get_connection_id_by_type(&ConnectionType::Location); for instance in instances { let Some(instance_id) = instance.id else { continue; @@ -265,6 +238,7 @@ pub struct LocationInfo { pub endpoint: String, pub active: bool, pub route_all_traffic: bool, + pub connection_type: ConnectionType, } #[tauri::command(async)] @@ -274,13 +248,8 @@ pub async fn all_locations( ) -> Result, Error> { debug!("Retrieving all locations."); let locations = Location::find_by_instance_id(&app_state.get_pool(), instance_id).await?; - let active_locations_ids: Vec = app_state - .active_connections - .lock() - .map_err(|_| Error::MutexError)? - .iter() - .map(|con| con.location_id) - .collect(); + let active_locations_ids: Vec = + app_state.get_connection_id_by_type(&ConnectionType::Location); let mut location_info = vec![]; for location in locations { let info = LocationInfo { @@ -291,6 +260,7 @@ pub async fn all_locations( endpoint: location.endpoint, active: active_locations_ids.contains(&location.id.expect("Missing location ID")), route_all_traffic: location.route_all_traffic, + connection_type: ConnectionType::Location, }; location_info.push(info); } @@ -323,60 +293,13 @@ pub struct LocationInterfaceDetails { #[tauri::command(async)] pub async fn location_interface_details( location_id: i64, + connection_type: ConnectionType, app_state: State<'_, AppState>, ) -> Result { - debug!("Fetching location details for location ID {location_id}"); let pool = app_state.get_pool(); - if let Some(location) = Location::find_by_id(&pool, location_id).await? { - debug!("Fetching WireGuard keys for location {}", location.name); - let keys = WireguardKeys::find_by_instance_id(&pool, location.instance_id) - .await? - .ok_or(Error::NotFound)?; - let peer_pubkey = keys.pubkey; - - // generate interface name - #[cfg(target_os = "macos")] - let interface_name = get_interface_name(); - #[cfg(not(target_os = "macos"))] - let interface_name = get_interface_name(&location); - - let result = query!( - r#" - SELECT last_handshake, listen_port as "listen_port!: u32", - persistent_keepalive_interval as "persistent_keepalive_interval?: u16" - FROM location_stats - WHERE location_id = $1 ORDER BY collected_at DESC LIMIT 1 - "#, - location_id - ) - .fetch_optional(&pool) - .await?; - - let (listen_port, persistent_keepalive_interval, last_handshake) = match result { - Some(record) => ( - Some(record.listen_port), - record.persistent_keepalive_interval, - Some(record.last_handshake), - ), - None => (None, None, None), - }; - - Ok(LocationInterfaceDetails { - location_id, - name: interface_name, - pubkey: location.pubkey, - address: location.address, - dns: location.dns, - listen_port, - peer_pubkey, - peer_endpoint: location.endpoint, - allowed_ips: location.allowed_ips, - persistent_keepalive_interval, - last_handshake, - }) - } else { - error!("Location ID {location_id} not found"); - Err(Error::NotFound) + match connection_type { + ConnectionType::Location => get_location_interface_details(location_id, &pool).await, + ConnectionType::Tunnel => get_tunnel_interface_details(location_id, &pool).await, } } @@ -466,23 +389,72 @@ fn get_aggregation(from: NaiveDateTime) -> Result { #[tauri::command] pub async fn location_stats( location_id: i64, + connection_type: ConnectionType, from: Option, app_state: State<'_, AppState>, -) -> Result, Error> { +) -> Result, Error> { trace!("Location stats command received"); let from = parse_timestamp(from)?.naive_utc(); let aggregation = get_aggregation(from)?; - LocationStats::all_by_location_id(&app_state.get_pool(), location_id, &from, &aggregation).await + let stats: Vec = match connection_type { + ConnectionType::Location => LocationStats::all_by_location_id( + &app_state.get_pool(), + location_id, + &from, + &aggregation, + ) + .await? + .into_iter() + .map(Into::into) + .collect(), + ConnectionType::Tunnel => { + TunnelStats::all_by_tunnel_id(&app_state.get_pool(), location_id, &from, &aggregation) + .await? + .into_iter() + .map(Into::into) + .collect() + } + }; + + Ok(stats) } #[tauri::command] pub async fn all_connections( location_id: i64, + connection_type: ConnectionType, app_state: State<'_, AppState>, -) -> Result, Error> { +) -> Result, Error> { + debug!("Retrieving connections for location {location_id}"); + let connections: Vec = match connection_type { + ConnectionType::Location => { + ConnectionInfo::all_by_location_id(&app_state.get_pool(), location_id) + .await? + .into_iter() + .map(Into::into) + .collect() + } + ConnectionType::Tunnel => { + TunnelConnectionInfo::all_by_tunnel_id(&app_state.get_pool(), location_id) + .await? + .into_iter() + .map(Into::into) + .collect() + } + }; + debug!("Connections received, returning."); + trace!("Connections found:\n{:#?}", connections); + Ok(connections) +} + +#[tauri::command] +pub async fn all_tunnel_connections( + location_id: i64, + app_state: State<'_, AppState>, +) -> Result, Error> { debug!("Retrieving connections for location {location_id}"); let connections = - ConnectionInfo::all_by_location_id(&app_state.get_pool(), location_id).await?; + TunnelConnectionInfo::all_by_tunnel_id(&app_state.get_pool(), location_id).await?; debug!("Connections received, returning."); trace!("Connections found:\n{:#?}", connections); Ok(connections) @@ -491,59 +463,93 @@ pub async fn all_connections( #[tauri::command] pub async fn active_connection( location_id: i64, + connection_type: ConnectionType, handle: AppHandle, ) -> Result, Error> { let state = handle.state::(); debug!("Retrieving active connection for location with id: {location_id}"); - if let Some(location) = Location::find_by_id(&state.get_pool(), location_id).await? { - debug!("Location found"); - let connection = state.find_connection(location.id.expect("Missing location ID")); - if connection.is_some() { - debug!("Active connection found"); - } - trace!("Connection:\n{:#?}", connection); - debug!("Connection returned"); - Ok(connection) - } else { - error!("Location with id: {location_id} not found."); - Err(Error::NotFound) + debug!("Location found"); + let connection = state.find_connection(location_id, connection_type); + if connection.is_some() { + debug!("Active connection found"); } + trace!("Connection:\n{:#?}", connection); + debug!("Connection returned"); + Ok(connection) } #[tauri::command] pub async fn last_connection( location_id: i64, + connection_type: ConnectionType, app_state: State<'_, AppState>, -) -> Result, Error> { - debug!("Retrieving last connection for location {location_id}"); - let connection = Connection::latest_by_location_id(&app_state.get_pool(), location_id).await?; - if connection.is_some() { +) -> Result, Error> { + debug!("Retrieving last connection for location {location_id} with type {connection_type:?}"); + if connection_type == ConnectionType::Location { + if let Some(connection) = + Connection::latest_by_location_id(&app_state.get_pool(), location_id).await? + { + trace!("Connection found"); + Ok(Some(connection.into())) + } else { + Ok(None) + } + } else if let Some(connection) = + TunnelConnection::latest_by_tunnel_id(&app_state.get_pool(), location_id).await? + { trace!("Connection found"); + Ok(Some(connection.into())) + } else { + Ok(None) } - Ok(connection) } #[tauri::command] pub async fn update_location_routing( location_id: i64, route_all_traffic: bool, + connection_type: ConnectionType, handle: AppHandle, -) -> Result { +) -> Result<(), Error> { let app_state = handle.state::(); - debug!("Updating location routing {location_id}"); - if let Some(mut location) = Location::find_by_id(&app_state.get_pool(), location_id).await? { - location.route_all_traffic = route_all_traffic; - location.save(&app_state.get_pool()).await?; - handle.emit_all( - "location-update", - Payload { - message: "Location routing updated".into(), - }, - )?; - Ok(location) - } else { - error!("Location with id: {location_id} not found."); - Err(Error::NotFound) + debug!("Updating location routing {location_id} with {connection_type:?}"); + + match connection_type { + ConnectionType::Location => { + if let Some(mut location) = + Location::find_by_id(&app_state.get_pool(), location_id).await? + { + location.route_all_traffic = route_all_traffic; + location.save(&app_state.get_pool()).await?; + handle.emit_all( + "location-update", + Payload { + message: "Location routing updated".into(), + }, + )?; + Ok(()) + } else { + error!("Location with id: {location_id} not found."); + Err(Error::NotFound) + } + } + ConnectionType::Tunnel => { + if let Some(mut tunnel) = Tunnel::find_by_id(&app_state.get_pool(), location_id).await? + { + tunnel.route_all_traffic = route_all_traffic; + tunnel.save(&app_state.get_pool()).await?; + handle.emit_all( + "location-update", + Payload { + message: "Tunnel routing updated".into(), + }, + )?; + Ok(()) + } else { + error!("Tunnel with id: {location_id} not found."); + Err(Error::NotFound) + } + } } } @@ -588,7 +594,9 @@ pub async fn delete_instance(instance_id: i64, handle: AppHandle) -> Result<(), let instance_locations = Location::find_by_instance_id(pool, instance_id).await?; for location in instance_locations.iter() { if let Some(location_id) = location.id { - if let Some(connection) = app_state.find_and_remove_connection(location_id) { + if let Some(connection) = + app_state.find_and_remove_connection(location_id, &ConnectionType::Location) + { debug!("Found active connection for location({location_id}), closing...",); let request = RemoveInterfaceRequest { interface_name: connection.interface_name.clone(), @@ -619,10 +627,17 @@ pub async fn parse_tunnel_config(config: String) -> Result { }) } #[tauri::command(async)] -pub async fn save_tunnel(mut tunnel: Tunnel, app_state: State<'_, AppState>) -> Result<(), Error> { +pub async fn save_tunnel(mut tunnel: Tunnel, handle: AppHandle) -> Result<(), Error> { + let app_state = handle.state::(); debug!("Received tunnel configuration: {tunnel:#?}"); tunnel.save(&app_state.get_pool()).await?; info!("Saved tunnel {tunnel:#?}"); + handle.emit_all( + "location-update", + Payload { + message: "Tunnel saved".into(), + }, + )?; Ok(()) } @@ -634,6 +649,7 @@ pub struct TunnelInfo { pub endpoint: String, pub active: bool, pub route_all_traffic: bool, + pub connection_type: ConnectionType, } #[tauri::command(async)] @@ -642,8 +658,9 @@ pub async fn all_tunnels(app_state: State<'_, AppState>) -> Result = vec![]; + let active_tunnel_ids: Vec = app_state.get_connection_id_by_type(&ConnectionType::Tunnel); for tunnel in tunnels { tunnel_info.push(TunnelInfo { @@ -652,7 +669,8 @@ pub async fn all_tunnels(app_state: State<'_, AppState>) -> Result, pub download: Option, } +impl From for CommonConnectionInfo { + fn from(val: ConnectionInfo) -> Self { + CommonConnectionInfo { + id: val.id, + location_id: val.location_id, + connected_from: val.connected_from, + start: val.start, + end: val.end, + upload: val.upload, + download: val.download, + } + } +} impl ConnectionInfo { pub async fn all_by_location_id(pool: &DbPool, location_id: i64) -> Result, Error> { @@ -129,16 +144,23 @@ pub struct ActiveConnection { pub connected_from: String, pub start: NaiveDateTime, pub interface_name: String, + pub connection_type: ConnectionType, } impl ActiveConnection { #[must_use] - pub fn new(location_id: i64, connected_from: String, interface_name: String) -> Self { + pub fn new( + location_id: i64, + connected_from: String, + interface_name: String, + connection_type: ConnectionType, + ) -> Self { let start = Utc::now().naive_utc(); Self { location_id, connected_from, start, interface_name, + connection_type, } } } @@ -154,3 +176,16 @@ impl From for Connection { } } } +// Implementing From for Connection into CommonConnection +impl From for CommonConnection { + fn from(connection: Connection) -> Self { + CommonConnection { + id: connection.id, + location_id: connection.location_id, + connected_from: connection.connected_from, + start: connection.start, + end: connection.end, + connection_type: ConnectionType::Location, + } + } +} diff --git a/src-tauri/src/database/models/location.rs b/src-tauri/src/database/models/location.rs index 0d4ae0d9..d40816b0 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -2,7 +2,10 @@ use chrono::{NaiveDateTime, Utc}; use sqlx::{query, query_as, Error as SqlxError, FromRow}; use std::time::SystemTime; -use crate::{commands::DateTimeAggregation, database::DbPool, error::Error}; +use crate::{ + commands::DateTimeAggregation, database::DbPool, error::Error, CommonLocationStats, + ConnectionType, +}; use defguard_wireguard_rs::host::Peer; use serde::{Deserialize, Serialize}; @@ -35,6 +38,22 @@ pub struct LocationStats { persistent_keepalive_interval: Option, } +impl From for CommonLocationStats { + fn from(location_stats: LocationStats) -> Self { + CommonLocationStats { + id: location_stats.id, + location_id: location_stats.location_id, + upload: location_stats.upload, + download: location_stats.download, + last_handshake: location_stats.last_handshake, + collected_at: location_stats.collected_at, + listen_port: location_stats.listen_port, + persistent_keepalive_interval: location_stats.persistent_keepalive_interval, + connection_type: ConnectionType::Location, + } + } +} + pub async fn peer_to_location_stats( peer: &Peer, listen_port: u32, diff --git a/src-tauri/src/database/models/tunnel.rs b/src-tauri/src/database/models/tunnel.rs index f4c1ad55..fa8808cc 100644 --- a/src-tauri/src/database/models/tunnel.rs +++ b/src-tauri/src/database/models/tunnel.rs @@ -1,4 +1,9 @@ -use crate::{commands::DateTimeAggregation, database::DbPool, error::Error}; +use crate::{ + commands::DateTimeAggregation, + database::{ActiveConnection, DbPool}, + error::Error, + CommonConnection, CommonConnectionInfo, CommonLocationStats, ConnectionType, +}; use chrono::{NaiveDateTime, Utc}; use defguard_wireguard_rs::host::Peer; use serde::{Deserialize, Serialize}; @@ -145,12 +150,12 @@ impl Tunnel { .await?; Ok(tunnels) } - pub async fn find_by_public_key(pool: &DbPool, pubkey: &str) -> Result { + pub async fn find_by_server_public_key(pool: &DbPool, pubkey: &str) -> Result { query_as!( Tunnel, "SELECT id \"id?\", name, pubkey, prvkey, address, server_pubkey, allowed_ips, endpoint, dns, persistent_keep_alive, route_all_traffic, pre_up, post_up, pre_down, post_down \ - FROM tunnel WHERE pubkey = $1;", + FROM tunnel WHERE server_pubkey = $1;", pubkey ) .fetch_one(pool) @@ -260,7 +265,7 @@ pub async fn peer_to_tunnel_stats( listen_port: u32, pool: &DbPool, ) -> Result { - let tunnel = Tunnel::find_by_public_key(pool, &peer.public_key.to_string()).await?; + let tunnel = Tunnel::find_by_server_public_key(pool, &peer.public_key.to_string()).await?; Ok(TunnelStats { id: None, tunnel_id: tunnel.id.unwrap(), @@ -285,6 +290,20 @@ pub struct TunnelConnection { pub end: NaiveDateTime, } +impl From for CommonConnectionInfo { + fn from(val: TunnelConnectionInfo) -> Self { + CommonConnectionInfo { + id: val.id, + location_id: val.tunnel_id, + connected_from: val.connected_from, + start: val.start, + end: val.end, + upload: val.upload, + download: val.download, + } + } +} + impl TunnelConnection { pub async fn save(&mut self, pool: &DbPool) -> Result<(), Error> { let result = query!( @@ -317,10 +336,7 @@ impl TunnelConnection { Ok(connections) } - pub async fn lastest_by_tunnel_id( - pool: &DbPool, - tunnel_id: i64, - ) -> Result, Error> { + pub async fn latest_by_tunnel_id(pool: &DbPool, tunnel_id: i64) -> Result, Error> { let connection = query_as!( TunnelConnection, r#" @@ -337,3 +353,100 @@ impl TunnelConnection { Ok(connection) } } + +/// Historical connection +#[derive(FromRow, Debug, Serialize)] +pub struct TunnelConnectionInfo { + pub id: i64, + pub tunnel_id: i64, + pub connected_from: String, + pub start: NaiveDateTime, + pub end: NaiveDateTime, + pub upload: Option, + pub download: Option, +} + +impl TunnelConnectionInfo { + pub async fn all_by_tunnel_id(pool: &DbPool, tunnel_id: i64) -> Result, Error> { + // Because we store interface information for given timestamp select last upload and download + // before connection ended + // FIXME: Optimize query + let connections = query_as!( + TunnelConnectionInfo, + r#" + SELECT + c.id as "id!", + c.tunnel_id as "tunnel_id!", + c.connected_from as "connected_from!", + c.start as "start!", + c.end as "end!", + COALESCE(( + SELECT ls.upload + FROM tunnel_stats AS ls + WHERE ls.tunnel_id = c.tunnel_id + AND ls.collected_at >= c.start + AND ls.collected_at <= c.end + ORDER BY ls.collected_at DESC + LIMIT 1 + ), 0) as "upload: _", + COALESCE(( + SELECT ls.download + FROM tunnel_stats AS ls + WHERE ls.tunnel_id = c.tunnel_id + AND ls.collected_at >= c.start + AND ls.collected_at <= c.end + ORDER BY ls.collected_at DESC + LIMIT 1 + ), 0) as "download: _" + FROM tunnel_connection AS c WHERE tunnel_id = $1 + ORDER BY start DESC; + "#, + tunnel_id + ) + .fetch_all(pool) + .await?; + + Ok(connections) + } +} +impl From for TunnelConnection { + fn from(active_connection: ActiveConnection) -> Self { + TunnelConnection { + id: None, + tunnel_id: active_connection.location_id, + connected_from: active_connection.connected_from, + start: active_connection.start, + end: Utc::now().naive_utc(), + } + } +} + +// Implementing From for TunnelConnection into CommonConnection +impl From for CommonConnection { + fn from(tunnel_connection: TunnelConnection) -> Self { + CommonConnection { + id: tunnel_connection.id, + location_id: tunnel_connection.tunnel_id, // Assuming you want to map tunnel_id to location_id + connected_from: tunnel_connection.connected_from, + start: tunnel_connection.start, + end: tunnel_connection.end, + connection_type: ConnectionType::Tunnel, // You need to set the connection_type appropriately based on your logic, + } + } +} +// Implement From trait for converting TunnelStats to CommonLocationStats +impl From for CommonLocationStats { + fn from(tunnel_stats: TunnelStats) -> Self { + CommonLocationStats { + id: tunnel_stats.id, + location_id: tunnel_stats.tunnel_id, + upload: tunnel_stats.upload, + download: tunnel_stats.download, + last_handshake: tunnel_stats.last_handshake, + collected_at: tunnel_stats.collected_at, + listen_port: tunnel_stats.listen_port, + persistent_keepalive_interval: tunnel_stats.persistent_keepalive_interval, // Set the appropriate value + connection_type: ConnectionType::Tunnel, + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 11b95f73..b5a1f466 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,5 @@ +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; pub mod appstate; pub mod commands; pub mod database; @@ -17,6 +19,8 @@ struct Payload { cwd: String, } +/// Location type used in commands to check if we using tunel or location +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] pub enum ConnectionType { Tunnel, Location, @@ -24,3 +28,54 @@ pub enum ConnectionType { #[macro_use] extern crate log; + +/// Common fields for Tunnel and Location +#[derive(Debug, Serialize, Deserialize)] +pub struct CommonWireguardFields { + pub instance_id: i64, + // Native id of network from defguard + pub network_id: i64, + pub name: String, + pub address: String, + pub pubkey: String, + pub endpoint: String, + pub allowed_ips: String, + pub dns: Option, + pub route_all_traffic: bool, +} + +/// Common fields for Connection and TunnelConnection due to shared command +#[derive(Debug, Serialize, Deserialize)] +pub struct CommonConnection { + pub id: Option, + pub location_id: i64, + pub connected_from: String, + pub start: NaiveDateTime, + pub end: NaiveDateTime, + pub connection_type: ConnectionType, +} + +// Common fields for LocationStats and TunnelStats due to shared command +#[derive(Debug, Serialize, Deserialize)] +pub struct CommonLocationStats { + pub id: Option, + pub location_id: i64, + pub upload: i64, + pub download: i64, + pub last_handshake: i64, + pub collected_at: NaiveDateTime, + pub listen_port: u32, + pub persistent_keepalive_interval: Option, + pub connection_type: ConnectionType, +} +// Common fields for ConnectionInfo and TunnelConnectionInfo due to shared command +#[derive(Debug, Serialize)] +pub struct CommonConnectionInfo { + pub id: i64, + pub location_id: i64, + pub connected_from: String, + pub start: NaiveDateTime, + pub end: NaiveDateTime, + pub upload: Option, + pub download: Option, +} diff --git a/src-tauri/src/service/log_watcher.rs b/src-tauri/src/service/log_watcher.rs index 659a1b43..17f6943a 100644 --- a/src-tauri/src/service/log_watcher.rs +++ b/src-tauri/src/service/log_watcher.rs @@ -4,7 +4,7 @@ //! The watcher monitors a given directory for any changes. Whenever a change is detected //! it parses the log files and sends logs relevant to a specified interface to the fronted. -use crate::{appstate::AppState, error::Error, utils::get_service_log_dir}; +use crate::{appstate::AppState, error::Error, utils::get_service_log_dir, ConnectionType}; use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; use notify_debouncer_mini::{ new_debouncer, @@ -253,6 +253,7 @@ pub async fn spawn_log_watcher_task( handle: AppHandle, location_id: i64, interface_name: String, + connection_type: ConnectionType, log_level: Level, from: Option, ) -> Result { @@ -262,8 +263,13 @@ pub async fn spawn_log_watcher_task( // parse `from` timestamp let from = from.and_then(|from| DateTime::::from_str(&from).ok()); - // FIXME: handle different naming for bare WireGuard tunnels once implemented - let event_topic = format!("log-update-location-{location_id}"); + let connection_type = if connection_type.eq(&ConnectionType::Tunnel) { + "Tunnel" + } else { + "Location" + }; + let event_topic = format!("log-update-{connection_type}-{location_id}"); + debug!("Using event topic: {event_topic}"); // explicitly clone before topic is moved into the closure let topic_clone = event_topic.clone(); diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 08e8efce..8a2cc595 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -3,20 +3,32 @@ use std::{ path::PathBuf, str::FromStr, }; +use tauri::AppHandle; use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration}; +use sqlx::query; use tauri::Manager; use tonic::{codegen::tokio_stream::StreamExt, transport::Channel}; use crate::{ appstate::AppState, - database::{models::location::peer_to_location_stats, DbPool, Location, WireguardKeys}, + commands::{LocationInterfaceDetails, Payload}, + database::{ + models::location::peer_to_location_stats, models::tunnel::peer_to_tunnel_stats, + ActiveConnection, DbPool, Location, Tunnel, WireguardKeys, + }, error::Error, - service::proto::{ - desktop_daemon_service_client::DesktopDaemonServiceClient, CreateInterfaceRequest, - ReadInterfaceDataRequest, + service::{ + log_watcher::spawn_log_watcher_task, + proto::{ + desktop_daemon_service_client::DesktopDaemonServiceClient, CreateInterfaceRequest, + ReadInterfaceDataRequest, + }, }, + ConnectionType, }; +use local_ip_address::local_ip; +use tracing::Level; pub static IS_MACOS: bool = cfg!(target_os = "macos"); pub static STATS_PERIOD: u64 = 60; @@ -144,8 +156,8 @@ pub fn get_interface_name() -> String { #[cfg(not(target_os = "macos"))] /// Returns interface name for location #[must_use] -pub fn get_interface_name(location: &Location) -> String { - remove_whitespace(&location.name) +pub fn get_interface_name(name: &str) -> String { + remove_whitespace(name) } fn is_port_free(port: u16) -> bool { @@ -159,7 +171,11 @@ fn is_port_free(port: u16) -> bool { } } -pub async fn spawn_stats_thread(handle: tauri::AppHandle, interface_name: String) { +pub async fn spawn_stats_thread( + handle: tauri::AppHandle, + interface_name: String, + connection_type: ConnectionType, +) { tokio::spawn(async move { let state = handle.state::(); let mut client = state.client.clone(); @@ -179,16 +195,29 @@ pub async fn spawn_stats_thread(handle: tauri::AppHandle, interface_name: String let peers: Vec = interface_data.peers.into_iter().map(Into::into).collect(); for peer in peers { - let mut location_stats = peer_to_location_stats( - &peer, - interface_data.listen_port, - &state.get_pool(), - ) - .await - .unwrap(); - debug!("Saving location stats: {location_stats:#?}"); - let _ = location_stats.save(&state.get_pool()).await; - debug!("Saved location stats: {location_stats:#?}"); + if connection_type.eq(&ConnectionType::Location) { + let mut location_stats = peer_to_location_stats( + &peer, + interface_data.listen_port, + &state.get_pool(), + ) + .await + .unwrap(); + debug!("Saving location stats: {location_stats:#?}"); + let _ = location_stats.save(&state.get_pool()).await; + debug!("Saved location stats: {location_stats:#?}"); + } else { + let mut tunnel_stats = peer_to_tunnel_stats( + &peer, + interface_data.listen_port, + &state.get_pool(), + ) + .await + .unwrap(); + debug!("Saving tunnel stats: {tunnel_stats:#?}"); + let _ = tunnel_stats.save(&state.get_pool()).await; + debug!("Saved location stats: {tunnel_stats:#?}"); + } } } Err(err) => { @@ -229,3 +258,320 @@ pub fn get_service_log_dir() -> PathBuf { path } +/// Setup client interface +pub async fn setup_interface_tunnel( + tunnel: &Tunnel, + interface_name: String, + mut client: DesktopDaemonServiceClient, +) -> Result<(), Error> { + // prepare peer config + debug!("Decoding location public key: {}.", tunnel.server_pubkey); + let peer_key: Key = Key::from_str(&tunnel.server_pubkey)?; + let mut peer = Peer::new(peer_key); + + debug!("Parsing location endpoint: {}", tunnel.endpoint); + let endpoint: SocketAddr = tunnel.endpoint.parse()?; + peer.endpoint = Some(endpoint); + peer.persistent_keepalive_interval = Some( + tunnel + .persistent_keep_alive + .try_into() + .expect("Failed to parse persistent keep alive"), + ); + + debug!("Parsing location allowed ips: {:?}", tunnel.allowed_ips); + let allowed_ips: Vec = if tunnel.route_all_traffic { + debug!("Using all traffic routing: {DEFAULT_ROUTE}"); + vec![DEFAULT_ROUTE.into()] + } else { + debug!("Using predefined location traffic"); + tunnel + .allowed_ips + .as_ref() + .map(|ips| ips.split(',').map(str::to_string).collect()) + .unwrap_or_default() + }; + for allowed_ip in &allowed_ips { + match IpAddrMask::from_str(allowed_ip) { + Ok(addr) => { + peer.allowed_ips.push(addr); + } + Err(err) => { + // Handle the error from IpAddrMask::from_str, if needed + error!("Error parsing IP address {allowed_ip}: {err}"); + // Continue to the next iteration of the loop + continue; + } + } + } + + // request interface configuration + if let Some(port) = find_random_free_port() { + let interface_config = InterfaceConfiguration { + name: interface_name, + prvkey: tunnel.prvkey.clone(), + address: tunnel.address.clone(), + port: port.into(), + peers: vec![peer.clone()], + }; + debug!("Creating interface {interface_config:#?}"); + let request = CreateInterfaceRequest { + config: Some(interface_config.clone().into()), + allowed_ips, + dns: tunnel.dns.clone(), + }; + if let Err(error) = client.create_interface(request).await { + error!("Failed to create interface: {error}"); + Err(Error::InternalError) + } else { + info!("Created interface {interface_config:#?}"); + Ok(()) + } + } else { + error!("Error finding free port"); + Err(Error::InternalError) + } +} + +pub async fn get_tunnel_interface_details( + tunnel_id: i64, + pool: &DbPool, +) -> Result { + debug!("Fetching tunnel details for tunnel ID {tunnel_id}"); + if let Some(tunnel) = Tunnel::find_by_id(pool, tunnel_id).await? { + debug!("Fetching WireGuard keys for location {}", tunnel.name); + let peer_pubkey = tunnel.pubkey; + + // generate interface name + #[cfg(target_os = "macos")] + let interface_name = get_interface_name(); + #[cfg(not(target_os = "macos"))] + let interface_name = get_interface_name(&tunnel.name); + + let result = query!( + r#" + SELECT last_handshake, listen_port as "listen_port!: u32", + persistent_keepalive_interval as "persistent_keepalive_interval?: u16" + FROM tunnel_stats + WHERE tunnel_id = $1 ORDER BY collected_at DESC LIMIT 1 + "#, + tunnel_id + ) + .fetch_optional(pool) + .await?; + + let (listen_port, persistent_keepalive_interval, last_handshake) = match result { + Some(record) => ( + Some(record.listen_port), + record.persistent_keepalive_interval, + Some(record.last_handshake), + ), + None => (None, None, None), + }; + + Ok(LocationInterfaceDetails { + location_id: tunnel_id, + name: interface_name, + pubkey: tunnel.server_pubkey, + address: tunnel.address, + dns: tunnel.dns, + listen_port, + peer_pubkey, + peer_endpoint: tunnel.endpoint, + allowed_ips: tunnel.allowed_ips.unwrap_or_default(), + persistent_keepalive_interval, + last_handshake, + }) + } else { + error!("Tunnel ID {tunnel_id} not found"); + Err(Error::NotFound) + } +} +pub async fn get_location_interface_details( + location_id: i64, + pool: &DbPool, +) -> Result { + debug!("Fetching location details for location ID {location_id}"); + if let Some(location) = Location::find_by_id(pool, location_id).await? { + debug!("Fetching WireGuard keys for location {}", location.name); + let keys = WireguardKeys::find_by_instance_id(pool, location.instance_id) + .await? + .ok_or(Error::NotFound)?; + let peer_pubkey = keys.pubkey; + + // generate interface name + #[cfg(target_os = "macos")] + let interface_name = get_interface_name(); + #[cfg(not(target_os = "macos"))] + let interface_name = get_interface_name(&location.name); + + let result = query!( + r#" + SELECT last_handshake, listen_port as "listen_port!: u32", + persistent_keepalive_interval as "persistent_keepalive_interval?: u16" + FROM location_stats + WHERE location_id = $1 ORDER BY collected_at DESC LIMIT 1 + "#, + location_id + ) + .fetch_optional(pool) + .await?; + + let (listen_port, persistent_keepalive_interval, last_handshake) = match result { + Some(record) => ( + Some(record.listen_port), + record.persistent_keepalive_interval, + Some(record.last_handshake), + ), + None => (None, None, None), + }; + + Ok(LocationInterfaceDetails { + location_id, + name: interface_name, + pubkey: location.pubkey, + address: location.address, + dns: location.dns, + listen_port, + peer_pubkey, + peer_endpoint: location.endpoint, + allowed_ips: location.allowed_ips, + persistent_keepalive_interval, + last_handshake, + }) + } else { + error!("Location ID {location_id} not found"); + Err(Error::NotFound) + } +} + +/// Setup new connection for location +pub async fn handle_connection_for_location( + location: &Location, + handle: AppHandle, +) -> Result<(), Error> { + debug!( + "Creating new interface connection for location: {}", + location.name + ); + let state = handle.state::(); + #[cfg(target_os = "macos")] + let interface_name = get_interface_name(); + #[cfg(not(target_os = "macos"))] + let interface_name = get_interface_name(&location.name); + setup_interface( + location, + interface_name.clone(), + &state.get_pool(), + state.client.clone(), + ) + .await?; + let address = local_ip()?; + let connection = ActiveConnection::new( + location.id.expect("Missing Location ID"), + address.to_string(), + interface_name.clone(), + ConnectionType::Location, + ); + state + .active_connections + .lock() + .map_err(|_| Error::MutexError)? + .push(connection); + debug!( + "Active connections: {:#?}", + state + .active_connections + .lock() + .map_err(|_| Error::MutexError)? + ); + debug!("Sending event connection-changed."); + handle.emit_all( + "connection-changed", + Payload { + message: "Created new connection".into(), + }, + )?; + + // Spawn stats threads + debug!("Spawning stats thread"); + spawn_stats_thread( + handle.clone(), + interface_name.clone(), + ConnectionType::Location, + ) + .await; + + // spawn log watcher + spawn_log_watcher_task( + handle, + location.id.expect("Missing Location ID"), + interface_name, + ConnectionType::Location, + Level::DEBUG, + None, + ) + .await?; + Ok(()) +} + +/// Setup new connection for tunnel +pub async fn handle_connection_for_tunnel(tunnel: &Tunnel, handle: AppHandle) -> Result<(), Error> { + debug!( + "Creating new interface connection for tunnel: {}", + tunnel.name + ); + let state = handle.state::(); + #[cfg(target_os = "macos")] + let interface_name = get_interface_name(); + #[cfg(not(target_os = "macos"))] + let interface_name = get_interface_name(&tunnel.name); + setup_interface_tunnel(tunnel, interface_name.clone(), state.client.clone()).await?; + let address = local_ip()?; + let connection = ActiveConnection::new( + tunnel.id.expect("Missing Tunnel ID"), + address.to_string(), + interface_name.clone(), + ConnectionType::Tunnel, + ); + state + .active_connections + .lock() + .map_err(|_| Error::MutexError)? + .push(connection); + debug!( + "Active connections: {:#?}", + state + .active_connections + .lock() + .map_err(|_| Error::MutexError)? + ); + debug!("Sending event connection-changed."); + handle.emit_all( + "connection-changed", + Payload { + message: "Created new connection".into(), + }, + )?; + + // Spawn stats threads + info!("Spawning stats thread"); + spawn_stats_thread( + handle.clone(), + interface_name.clone(), + ConnectionType::Tunnel, + ) + .await; + + //spawn log watcher + spawn_log_watcher_task( + handle, + tunnel.id.expect("Missing Tunnel ID"), + interface_name, + ConnectionType::Tunnel, + Level::DEBUG, + None, + ) + .await?; + Ok(()) +} diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 11a86d92..405cf484 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -27,7 +27,6 @@ import { ClientAddInstancePage } from '../../pages/client/pages/ClientAddInstanc import { ClientAddTunnelPage } from '../../pages/client/pages/ClientAddTunnelPage/ClientAddTunnelPage'; import { ClientInstancePage } from '../../pages/client/pages/ClientInstancePage/ClientInstancePage'; import { ClientSettingsPage } from '../../pages/client/pages/ClientSettingsPage/ClientSettingsPage'; -import { ClientTunnelPage } from '../../pages/client/pages/ClientTunnelPage/ClientTunnelPage'; import { EnrollmentPage } from '../../pages/enrollment/EnrollmentPage'; import { SessionTimeoutPage } from '../../pages/sessionTimeout/SessionTimeoutPage'; import { ToastManager } from '../../shared/defguard-ui/components/Layout/ToastManager/ToastManager'; @@ -76,10 +75,6 @@ const router = createBrowserRouter([ path: '/client/add-tunnel', element: , }, - { - path: '/client/tunnel', - element: , - }, { path: '/client/settings', element: , diff --git a/src/pages/client/ClientPage.tsx b/src/pages/client/ClientPage.tsx index 3e1d9159..5b16898b 100644 --- a/src/pages/client/ClientPage.tsx +++ b/src/pages/client/ClientPage.tsx @@ -11,11 +11,14 @@ import { useClientStore } from './hooks/useClientStore'; import { clientQueryKeys } from './query'; import { TauriEventKey } from './types'; -const { getInstances } = clientApi; +const { getInstances, getTunnels } = clientApi; export const ClientPage = () => { const queryClient = useQueryClient(); - const setInstances = useClientStore((state) => state.setInstances); + const [setInstances, setTunnels] = useClientStore((state) => [ + state.setInstances, + state.setTunnels, + ]); const { data: instances } = useQuery({ queryFn: getInstances, @@ -23,12 +26,22 @@ export const ClientPage = () => { refetchOnMount: true, refetchOnWindowFocus: false, }); + const { data: tunnels } = useQuery({ + queryFn: getTunnels, + queryKey: [clientQueryKeys.getTunnels], + refetchOnMount: true, + refetchOnWindowFocus: false, + }); useEffect(() => { const subs: UnlistenFn[] = []; listen(TauriEventKey.INSTANCE_UPDATE, () => { - const invalidate = [clientQueryKeys.getInstances, clientQueryKeys.getLocations]; + const invalidate = [ + clientQueryKeys.getInstances, + clientQueryKeys.getLocations, + clientQueryKeys.getTunnels, + ]; invalidate.forEach((key) => queryClient.invalidateQueries({ queryKey: [key], @@ -39,7 +52,7 @@ export const ClientPage = () => { }); listen(TauriEventKey.LOCATION_UPDATE, () => { - const invalidate = [clientQueryKeys.getLocations]; + const invalidate = [clientQueryKeys.getLocations, clientQueryKeys.getTunnels]; invalidate.forEach((key) => queryClient.invalidateQueries({ queryKey: [key], @@ -57,6 +70,7 @@ export const ClientPage = () => { clientQueryKeys.getConnectionHistory, clientQueryKeys.getLocationStats, clientQueryKeys.getInstances, + clientQueryKeys.getTunnels, ]; invalidate.forEach((key) => queryClient.invalidateQueries({ @@ -77,7 +91,10 @@ export const ClientPage = () => { if (instances) { setInstances(instances); } - }, [instances, setInstances]); + if (tunnels) { + setTunnels(tunnels); + } + }, [instances, setInstances, tunnels, setTunnels]); return ( <> diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index cbd29899..0e0f30b6 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -1,6 +1,6 @@ import { ThemeKey } from '../../../shared/defguard-ui/hooks/theme/types'; import { CreateDeviceResponse } from '../../../shared/hooks/api/types'; -import { DefguardInstance, DefguardLocation } from '../types'; +import { DefguardInstance, DefguardLocation, WireguardInstanceType } from '../types'; export type GetLocationsRequest = { instanceId: number; @@ -8,15 +8,18 @@ export type GetLocationsRequest = { export type ConnectionRequest = { locationId: number; + connectionType: WireguardInstanceType; }; export type RoutingRequest = { locationId: number; + connectionType: WireguardInstanceType; routeAllTraffic?: boolean; }; export type StatsRequest = { locationId: number; + connectionType: WireguardInstanceType; from?: string; }; @@ -98,6 +101,7 @@ export type TunnelRequest = { export type LocationDetailsRequest = { locationId: number; + connectionType: WireguardInstanceType; }; export type TauriCommandKey = diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index 2d4398f7..36b6c81e 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -57,7 +57,7 @@ export const ClientSideBar = () => { type: WireguardInstanceType.TUNNEL, }, }); - navigate(routes.client.tunnelPage, { replace: true }); + navigate(routes.client.base, { replace: true }); }} > diff --git a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx index 02fe316d..2371b99f 100644 --- a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx +++ b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx @@ -25,13 +25,8 @@ export const ClientBarItem = ({ instance }: Props) => const setClientStore = useClientStore((state) => state.setState); const selectedInstance = useClientStore((state) => state.selectedInstance); - // FIXME: Fix tunnel active when detail will be implemented const active = - instance.type === WireguardInstanceType.TUNNEL - ? routes.client.tunnelPage + instance.id === window.location.pathname - : instance.type === WireguardInstanceType.DEFGUARD_INSTANCE - ? instance.id === selectedInstance?.id - : false; + instance.type === selectedInstance?.type && instance.id === selectedInstance.id; const cn = classNames('client-bar-item', 'clickable', { active: active, @@ -57,9 +52,6 @@ export const ClientBarItem = ({ instance }: Props) => type: WireguardInstanceType.DEFGUARD_INSTANCE, }, }); - if (!instancePage) { - navigate(routes.client.base, { replace: true }); - } } else { setClientStore({ selectedInstance: { @@ -67,7 +59,9 @@ export const ClientBarItem = ({ instance }: Props) => type: WireguardInstanceType.TUNNEL, }, }); - navigate(routes.client.tunnelPage); + } + if (!instancePage) { + navigate(routes.client.base, { replace: true }); } }} > diff --git a/src/pages/client/hooks/useClientStore.tsx b/src/pages/client/hooks/useClientStore.tsx index 90180da8..a501f8bd 100644 --- a/src/pages/client/hooks/useClientStore.tsx +++ b/src/pages/client/hooks/useClientStore.tsx @@ -42,6 +42,16 @@ export const useClientStore = createWithEqualityFn( } return set({ instances: values }); }, + setTunnels: (values) => { + if (isUndefined(get().selectedInstance)) { + return set({ + tunnels: values, + selectedInstance: + { id: values[0]?.id, type: WireguardInstanceType.TUNNEL } ?? undefined, + }); + } + return set({ tunnels: values }); + }, updateInstances: async () => { const res = await getInstances(); let selected = get().selectedInstance; @@ -78,6 +88,7 @@ type StoreValues = { type StoreMethods = { setState: (values: Partial) => void; setInstances: (instances: DefguardInstance[]) => void; + setTunnels: (tunnels: CommonWireguardFields[]) => void; updateInstances: () => Promise; updateSettings: (data: Partial) => Promise; }; diff --git a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx index e46a1cf3..beadadba 100644 --- a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx +++ b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx @@ -8,6 +8,7 @@ import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/ import { ButtonStyleVariant } from '../../../../shared/defguard-ui/components/Layout/Button/types'; import { routes } from '../../../../shared/routes'; import { useClientStore } from '../../hooks/useClientStore'; +import { WireguardInstanceType } from '../../types'; import { LocationsList } from './components/LocationsList/LocationsList'; import { StatsFilterSelect } from './components/StatsFilterSelect/StatsFilterSelect'; import { StatsLayoutSelect } from './components/StatsLayoutSelect/StatsLayoutSelect'; @@ -17,15 +18,21 @@ import { useUpdateInstanceModal } from './modals/UpdateInstanceModal/useUpdateIn export const ClientInstancePage = () => { const { LL } = useI18nContext(); - const pageLL = LL.pages.client.pages.instancePage; + const instanceLL = LL.pages.client.pages.instancePage; + const tunelLL = LL.pages.client.pages.tunnelPage; const instances = useClientStore((state) => state.instances); - const selectedInstanceId = useClientStore((state) => state.selectedInstance?.id); + const [selectedInstanceId, selectedInstanceType] = useClientStore((state) => [ + state.selectedInstance?.id, + state.selectedInstance?.type, + ]); const selectedInstance = useMemo( () => instances.find((i) => i.id === selectedInstanceId), [instances, selectedInstanceId], ); const navigate = useNavigate(); + const isLocationPage = selectedInstanceType === WireguardInstanceType.TUNNEL; + const openUpdateInstanceModal = useUpdateInstanceModal((state) => state.open); // router guard, if no instances redirect to add instance, for now, later this will be replaced by init welcome flow @@ -38,20 +45,24 @@ export const ClientInstancePage = () => { return (
-

{pageLL.title()}

+

{!isLocationPage ? instanceLL.title() : tunelLL.title()}

- -
diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx index 0c017679..1faf9ae2 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx @@ -1,8 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useI18nContext } from '../../../../../../i18n/i18n-react'; import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import { routes } from '../../../../../../shared/routes'; import { clientApi } from '../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../query'; @@ -20,6 +22,8 @@ export const LocationsList = () => { const toaster = useToaster(); + const navigate = useNavigate(); + const queryKey = useMemo(() => { if (selectedInstance?.type === WireguardInstanceType.DEFGUARD_INSTANCE) { return [clientQueryKeys.getLocations, selectedInstance?.id as number]; @@ -48,21 +52,42 @@ export const LocationsList = () => { } }, [isError, toaster, LL.common.messages]); + useEffect(() => { + if ( + locations?.length === 0 && + selectedInstance?.type === WireguardInstanceType.TUNNEL + ) { + navigate(routes.client.addTunnel, { replace: true }); + } + }, [locations, navigate, selectedInstance]); + // TODO: add loader or another placeholder view pointing to opening enter token modal if no instances are found / present if (!selectedInstance || !locations) return null; return ( <> {selectedView === ClientView.GRID && - (selectedInstance.id || - selectedInstance.type === WireguardInstanceType.TUNNEL) && ( - - )} + selectedInstance.type === WireguardInstanceType.DEFGUARD_INSTANCE && + selectedInstance.id !== null && } + + {selectedInstance.type === WireguardInstanceType.TUNNEL && + selectedInstance.id === undefined && } + {selectedView === ClientView.DETAIL && selectedInstance.id && selectedInstance.type === WireguardInstanceType.DEFGUARD_INSTANCE && ( - + )} + + {selectedInstance.id && selectedInstance.type === WireguardInstanceType.TUNNEL && ( + + )} ); }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx index 490eeb58..bfb76d00 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardConnectButton/LocationCardConnectButton.tsx @@ -36,10 +36,14 @@ export const LocationCardConnectButton = ({ location }: Props) => { try { if (location) { if (location?.active) { - await disconnect({ locationId: location.id }); + await disconnect({ + locationId: location.id, + connectionType: location.connection_type, + }); } else { await connect({ locationId: location?.id, + connectionType: location.connection_type, }); } setIsLoading(false); diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/LocationCardInfo.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/LocationCardInfo.tsx index c87bff9d..1dec96d6 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/LocationCardInfo.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/LocationCardInfo.tsx @@ -6,7 +6,11 @@ import dayjs from 'dayjs'; import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { clientQueryKeys } from '../../../../../../query'; -import { CommonWireguardFields, Connection } from '../../../../../../types'; +import { + CommonWireguardFields, + Connection, + WireguardInstanceType, +} from '../../../../../../types'; type Props = { location?: CommonWireguardFields; @@ -20,8 +24,16 @@ export const LocationCardInfo = ({ location, connection }: Props) => { const localLL = LL.pages.client.pages.instancePage.connectionLabels; const { data: activeConnection } = useQuery({ - queryKey: [clientQueryKeys.getActiveConnection, location?.id as number], - queryFn: () => getActiveConnection({ locationId: location?.id as number }), + queryKey: [ + clientQueryKeys.getActiveConnection, + location?.id as number, + location?.connection_type, + ], + queryFn: () => + getActiveConnection({ + locationId: location?.id as number, + connectionType: location?.connection_type as WireguardInstanceType, + }), enabled: location?.active, }); diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx index 66464d93..cf82d5a7 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx @@ -17,9 +17,10 @@ const { updateLocationRouting } = clientApi; export const LocationCardRoute = ({ location }: Props) => { const handleChange = async (value: boolean) => { try { - if (location) { + if (location && location.connection_type) { await updateLocationRouting({ locationId: location?.id, + connectionType: location.connection_type, routeAllTraffic: value, }); } diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx index 60aa6a0a..68eaeb4e 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx @@ -1,5 +1,6 @@ import './style.scss'; +import { useQuery } from '@tanstack/react-query'; import { isUndefined } from 'lodash-es'; import { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -7,13 +8,17 @@ import { useNavigate } from 'react-router-dom'; import { CardTabs } from '../../../../../../../../shared/defguard-ui/components/Layout/CardTabs/CardTabs'; import { CardTabsData } from '../../../../../../../../shared/defguard-ui/components/Layout/CardTabs/types'; import { routes } from '../../../../../../../../shared/routes'; -import { CommonWireguardFields } from '../../../../../../types'; +import { clientApi } from '../../../../../../clientAPI/clientApi'; +import { useClientStore } from '../../../../../../hooks/useClientStore'; +import { clientQueryKeys } from '../../../../../../query'; +import { CommonWireguardFields, WireguardInstanceType } from '../../../../../../types'; import { LocationConnectionHistory } from './components/LocationConnectionHistory/LocationConnectionHistory'; import { LocationDetailCard } from './components/LocationDetailCard/LocationDetailCard'; import { LocationDetails } from './components/LocationDetails/LocationDetails'; type Props = { locations: CommonWireguardFields[]; + connectionType?: WireguardInstanceType; }; const findLocationById = ( @@ -21,10 +26,18 @@ const findLocationById = ( id: number, ): CommonWireguardFields | undefined => locations.find((location) => location.id === id); -export const LocationsDetailView = ({ locations }: Props) => { +const { getTunnels } = clientApi; + +export const LocationsDetailView = ({ + locations, + connectionType = WireguardInstanceType.DEFGUARD_INSTANCE, +}: Props) => { const [activeLocationId, setActiveLocationId] = useState( locations[0]?.id ?? undefined, ); + + const selectedInstance = useClientStore((state) => state.selectedInstance); + const navigate = useNavigate(); const tabs = useMemo( @@ -51,17 +64,55 @@ export const LocationsDetailView = ({ locations }: Props) => { } }, [activeLocationId, navigate]); + const { data: tunnels } = useQuery({ + queryKey: [clientQueryKeys.getTunnels], + queryFn: () => getTunnels(), + enabled: !!( + selectedInstance?.id && selectedInstance?.type === WireguardInstanceType.TUNNEL + ), + }); + + const tunnel = tunnels?.find((tunnel) => tunnel.id === selectedInstance?.id); + return (
- - {activeLocation && } - {activeLocation && ( - + {connectionType === WireguardInstanceType.DEFGUARD_INSTANCE && ( + <> + + {activeLocation && } + {activeLocation && ( + + )} + {activeLocation && ( + + )} + + )} + {connectionType === WireguardInstanceType.TUNNEL && ( + <> + {tunnel && } + {tunnel && ( + + )} + {tunnel && ( + + )} + )} - {activeLocation && }
); }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx index 6f42f77a..a310cf2c 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx @@ -6,24 +6,29 @@ import { useI18nContext } from '../../../../../../../../../../i18n/i18n-react'; import { Card } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Card/Card'; import { clientApi } from '../../../../../../../../clientAPI/clientApi'; import { clientQueryKeys } from '../../../../../../../../query'; -import { DefguardLocation } from '../../../../../../../../types'; +import { DefguardLocation, WireguardInstanceType } from '../../../../../../../../types'; import { LocationCardNeverConnected } from '../../../LocationCardNeverConnected/LocationCardNeverConnected'; import { LocationHistoryTable } from './LocationHistoryTable/LocationHistoryTable'; type Props = { locationId: DefguardLocation['id']; + connectionType: WireguardInstanceType; connected: boolean; }; const { getConnectionHistory } = clientApi; -export const LocationConnectionHistory = ({ locationId, connected }: Props) => { +export const LocationConnectionHistory = ({ + locationId, + connectionType, + connected, +}: Props) => { const { LL } = useI18nContext(); const localLL = LL.pages.client.pages.instancePage.detailView.history; const { data: connectionHistory } = useQuery({ queryKey: [clientQueryKeys.getConnectionHistory, locationId], - queryFn: () => getConnectionHistory({ locationId }), + queryFn: () => getConnectionHistory({ locationId, connectionType }), enabled: !!locationId, refetchInterval: 10 * 1000, refetchOnWindowFocus: true, diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx index 0728a033..086db445 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx @@ -38,10 +38,16 @@ export const LocationDetailCard = memo(({ location, tabbed = false }: Props) => const statsFilter = useClientStore((state) => state.statsFilter); const { data: locationStats } = useQuery({ - queryKey: [clientQueryKeys.getLocationStats, location.id, statsFilter], + queryKey: [ + clientQueryKeys.getLocationStats, + location.id, + statsFilter, + location.connection_type, + ], queryFn: () => getLocationStats({ locationId: location.id, + connectionType: location.connection_type, from: getStatsFilterValue(statsFilter), }), enabled: !!location, @@ -51,8 +57,12 @@ export const LocationDetailCard = memo(({ location, tabbed = false }: Props) => }); const { data: lastConnection } = useQuery({ - queryKey: [clientQueryKeys.getConnections, location.id], - queryFn: () => getLastConnection({ locationId: location.id }), + queryKey: [clientQueryKeys.getConnections, location.id, location.connection_type], + queryFn: () => + getLastConnection({ + locationId: location.id, + connectionType: location.connection_type, + }), enabled: !!location, refetchInterval: 10 * 1000, refetchOnWindowFocus: true, diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx index 790937e1..a6b464cb 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx @@ -10,16 +10,17 @@ import { Divider } from '../../../../../../../../../../shared/defguard-ui/compon import { Label } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Label/Label'; import { clientApi } from '../../../../../../../../clientAPI/clientApi'; import { clientQueryKeys } from '../../../../../../../../query'; -import { DefguardLocation } from '../../../../../../../../types'; +import { DefguardLocation, WireguardInstanceType } from '../../../../../../../../types'; import { LocationLogs } from '../LocationLogs/LocationLogs'; type Props = { locationId: DefguardLocation['id']; + connectionType: WireguardInstanceType; }; const { getLocationDetails } = clientApi; -export const LocationDetails = ({ locationId }: Props) => { +export const LocationDetails = ({ locationId, connectionType }: Props) => { const { LL } = useI18nContext(); const localLL = LL.pages.client.pages.instancePage.detailView.details; @@ -28,19 +29,19 @@ export const LocationDetails = ({ locationId }: Props) => {

{localLL.title()}

- - + + ); }; -const InfoSection = memo(({ locationId }: Props) => { +const InfoSection = memo(({ locationId, connectionType }: Props) => { const { LL } = useI18nContext(); const localLL = LL.pages.client.pages.instancePage.detailView.details; const { data } = useQuery({ - queryKey: [clientQueryKeys.getLocationDetails, locationId], - queryFn: () => getLocationDetails({ locationId }), + queryKey: [clientQueryKeys.getLocationDetails, locationId, connectionType], + queryFn: () => getLocationDetails({ locationId, connectionType }), enabled: !!locationId, refetchInterval: 1000, }); diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx index 97bd4234..9c5bbaf6 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx @@ -13,14 +13,15 @@ import { ActionButtonVariant } from '../../../../../../../../../../shared/defgua import { Card } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Card/Card'; import { LogItem, LogLevel } from '../../../../../../../../clientAPI/types'; import { useClientStore } from '../../../../../../../../hooks/useClientStore'; -import { DefguardLocation } from '../../../../../../../../types'; +import { DefguardLocation, WireguardInstanceType } from '../../../../../../../../types'; import { LocationLogsSelect } from './LocationLogsSelect'; type Props = { locationId: DefguardLocation['id']; + connectionType: WireguardInstanceType; }; -export const LocationLogs = ({ locationId }: Props) => { +export const LocationLogs = ({ locationId, connectionType }: Props) => { const logsContainerElement = useRef(null); const appLogLevel = useClientStore((state) => state.settings.log_level); const locationLogLevelRef = useRef(appLogLevel); @@ -40,7 +41,7 @@ export const LocationLogs = ({ locationId }: Props) => { let eventUnlisten: UnlistenFn; const startLogListen = async () => { eventUnlisten = await listen( - `log-update-location-${locationId}`, + `log-update-${connectionType}-${locationId}`, ({ payload: logItems }) => { if (logsContainerElement.current) { logItems.forEach((item) => { diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx index 838f9f1d..198db581 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx @@ -12,7 +12,7 @@ import { getStatsFilterValue } from '../../../../../../../../shared/utils/getSta import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../../../query'; -import { CommonWireguardFields } from '../../../../../../types'; +import { CommonWireguardFields, WireguardInstanceType } from '../../../../../../types'; import { LocationUsageChart } from '../../../LocationUsageChart/LocationUsageChart'; import { LocationUsageChartType } from '../../../LocationUsageChart/types'; import { LocationCardConnectButton } from '../LocationCardConnectButton/LocationCardConnectButton'; @@ -54,15 +54,29 @@ const GridItem = ({ location }: GridItemProps) => { const statsFilter = useClientStore((state) => state.statsFilter); const { data: lastConnection } = useQuery({ - queryKey: [clientQueryKeys.getConnections, location.id as number], - queryFn: () => getLastConnection({ locationId: location.id as number }), + queryKey: [ + clientQueryKeys.getConnections, + location.id as number, + location.connection_type, + ], + queryFn: () => + getLastConnection({ + locationId: location.id as number, + connectionType: location.connection_type, + }), enabled: !!location.id, }); const { data: locationStats } = useQuery({ - queryKey: [clientQueryKeys.getLocationStats, location.id as number, statsFilter], + queryKey: [ + clientQueryKeys.getLocationStats, + location.id as number, + statsFilter, + location.connection_type, + ], queryFn: () => getLocationStats({ locationId: location.id as number, + connectionType: location.connection_type as WireguardInstanceType, from: getStatsFilterValue(statsFilter), }), enabled: !!location.id, diff --git a/src/pages/client/pages/ClientTunnelPage/ClientTunnelPage.tsx b/src/pages/client/pages/ClientTunnelPage/ClientTunnelPage.tsx deleted file mode 100644 index cd5fa96f..00000000 --- a/src/pages/client/pages/ClientTunnelPage/ClientTunnelPage.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import './style.scss'; - -import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import { useI18nContext } from '../../../../i18n/i18n-react'; -import { routes } from '../../../../shared/routes'; -import { useClientStore } from '../../hooks/useClientStore'; -import { LocationsList } from '../ClientInstancePage/components/LocationsList/LocationsList'; -import { StatsFilterSelect } from '../ClientInstancePage/components/StatsFilterSelect/StatsFilterSelect'; - -export const ClientTunnelPage = () => { - const { LL } = useI18nContext(); - const pageLL = LL.pages.client.pages.tunnelPage; - const tunnels = useClientStore((state) => state.tunnels); - const navigate = useNavigate(); - - // router guard, if no tunnels redirect to add tunnel - useEffect(() => { - if (tunnels.length === 0) { - navigate(routes.client.addTunnel, { replace: true }); - } - }, [tunnels, navigate]); - - return ( -
-
-

{pageLL.title()}

-
- -
-
- -
- ); -}; diff --git a/src/pages/client/pages/ClientTunnelPage/style.scss b/src/pages/client/pages/ClientTunnelPage/style.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts index 2ca92139..a235a833 100644 --- a/src/pages/client/types.ts +++ b/src/pages/client/types.ts @@ -52,7 +52,8 @@ export type CommonWireguardFields = { route_all_traffic: boolean; // Connected active: boolean; - type?: WireguardInstanceType; + // Tunnel or Location + connection_type: WireguardInstanceType; }; export enum ClientView { diff --git a/src/shared/routes.ts b/src/shared/routes.ts index bc3c2d6b..868d0535 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -6,7 +6,6 @@ export const routes = { instancePage: '/client/', addInstance: '/client/add-instance', addTunnel: '/client/add-tunnel', - tunnelPage: '/client/tunnel', settings: '/client/settings', }, enrollment: '/enrollment', From 92ef11fb6706b9acad8c6d1a23bf3f6c46b1f03b Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Wed, 3 Jan 2024 17:19:14 +0100 Subject: [PATCH 18/45] feat: Edit tunnel (#142) * Edit tunnel page also fix saving connections on client discconection --------- Co-authored-by: akantorczyk --- src-tauri/src/appstate.rs | 23 +- src-tauri/src/bin/defguard-client.rs | 9 +- src-tauri/src/commands.rs | 14 + src/components/App/App.tsx | 5 + src/i18n/en/index.ts | 14 + src/i18n/i18n-types.ts | 56 ++++ src/pages/client/clientAPI/clientApi.ts | 5 + src/pages/client/clientAPI/types.ts | 1 + .../ClientSideBar/ClientSideBar.tsx | 17 +- .../components/ClientSideBar/style.scss | 1 + .../AddTunnelFormCard/AddTunnelFormCard.tsx | 31 +- .../ClientEditTunnelPage.tsx | 69 ++++ .../components/EditTunnelFormCard.tsx | 298 ++++++++++++++++++ .../pages/ClientEditTunnelPage/style.scss | 109 +++++++ .../ClientInstancePage/ClientInstancePage.tsx | 16 +- src/shared/routes.ts | 1 + 16 files changed, 644 insertions(+), 25 deletions(-) create mode 100644 src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx create mode 100644 src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx create mode 100644 src/pages/client/pages/ClientEditTunnelPage/style.scss diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index 89a36dd5..8b975fdb 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -7,7 +7,7 @@ use tokio_util::sync::CancellationToken; use tonic::transport::Channel; use crate::{ - database::{ActiveConnection, Connection, DbPool}, + database::{ActiveConnection, Connection, DbPool, TunnelConnection}, error::Error, service::{ proto::{ @@ -111,11 +111,22 @@ impl AppState { debug!("Removed interface"); debug!("Saving connection"); trace!("Connection: {connection:#?}"); - let mut connection: Connection = connection.into(); - connection.save(&self.get_pool()).await?; - debug!("Connection saved"); - trace!("Saved connection: {connection:#?}"); - info!("Location {} disconnected", connection.location_id); + match connection.connection_type { + ConnectionType::Location => { + let mut connection: Connection = connection.into(); + connection.save(&self.get_pool()).await?; + debug!("Connection saved"); + trace!("Saved connection: {connection:#?}"); + info!("Location {} disconnected", connection.location_id); + } + ConnectionType::Tunnel => { + let mut connection: TunnelConnection = connection.into(); + connection.save(&self.get_pool()).await?; + debug!("Connection saved"); + trace!("Saved connection: {connection:#?}"); + info!("Tunnel {} disconnected", connection.tunnel_id); + } + } } Ok(()) } diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 026cfbe4..f280bcea 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -15,14 +15,14 @@ use defguard_client::{ __cmd__all_tunnels, __cmd__connect, __cmd__delete_instance, __cmd__disconnect, __cmd__get_settings, __cmd__last_connection, __cmd__location_interface_details, __cmd__location_stats, __cmd__parse_tunnel_config, __cmd__save_device_config, - __cmd__save_tunnel, __cmd__update_instance, __cmd__update_location_routing, - __cmd__update_settings, + __cmd__save_tunnel, __cmd__tunnel_details, __cmd__update_instance, + __cmd__update_location_routing, __cmd__update_settings, appstate::AppState, commands::{ active_connection, all_connections, all_instances, all_locations, all_tunnels, connect, delete_instance, disconnect, get_settings, last_connection, location_interface_details, - location_stats, parse_tunnel_config, save_device_config, save_tunnel, update_instance, - update_location_routing, update_settings, + location_stats, parse_tunnel_config, save_device_config, save_tunnel, tunnel_details, + update_instance, update_location_routing, update_settings, }, database::{self, models::settings::Settings}, tray::{configure_tray_icon, create_tray_menu, handle_tray_event}, @@ -94,6 +94,7 @@ async fn main() { parse_tunnel_config, save_tunnel, all_tunnels, + tunnel_details, ]) .on_window_event(|event| match event.event() { tauri::WindowEvent::CloseRequested { api, .. } => { diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1bd8860d..6d777560 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -675,3 +675,17 @@ pub async fn all_tunnels(app_state: State<'_, AppState>) -> Result, +) -> Result { + debug!("Retrieving Tunnel with ID {tunnel_id}."); + + if let Some(tunnel) = Tunnel::find_by_id(&app_state.get_pool(), tunnel_id).await? { + Ok(tunnel) + } else { + error!("Tunnel with ID: {tunnel_id}, not found"); + Err(Error::NotFound) + } +} diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 405cf484..5e364910 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -25,6 +25,7 @@ import { ClientPage } from '../../pages/client/ClientPage'; import { useClientStore } from '../../pages/client/hooks/useClientStore'; import { ClientAddInstancePage } from '../../pages/client/pages/ClientAddInstancePage/ClientAddInstnacePage'; import { ClientAddTunnelPage } from '../../pages/client/pages/ClientAddTunnelPage/ClientAddTunnelPage'; +import { ClientEditTunnelPage } from '../../pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage'; import { ClientInstancePage } from '../../pages/client/pages/ClientInstancePage/ClientInstancePage'; import { ClientSettingsPage } from '../../pages/client/pages/ClientSettingsPage/ClientSettingsPage'; import { EnrollmentPage } from '../../pages/enrollment/EnrollmentPage'; @@ -75,6 +76,10 @@ const router = createBrowserRouter([ path: '/client/add-tunnel', element: , }, + { + path: '/client/edit-tunnel', + element: , + }, { path: '/client/settings', element: , diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 1a83e0f4..9badd2d8 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -168,6 +168,20 @@ const en = { }, tunnelPage: { title: 'WireGuard Tunnels', + header: { + edit: 'Edit Tunnel', + }, + }, + + editTunnelPage: { + title: 'Edit WireGuard® Tunnel', + messages: { + editSuccess: 'Tunnel edited', + editError: 'Editing tunnel failed', + }, + controls: { + save: 'Save changes', + }, }, addTunnelPage: { title: 'Add WireGuard® Tunnel', diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 1c42ab0b..916b90c1 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -422,6 +422,34 @@ type RootTranslation = { * W​i​r​e​G​u​a​r​d​ ​T​u​n​n​e​l​s */ title: string + header: { + /** + * E​d​i​t​ ​T​u​n​n​e​l + */ + edit: string + } + } + editTunnelPage: { + /** + * E​d​i​t​ ​W​i​r​e​G​u​a​r​d​®​ ​T​u​n​n​e​l + */ + title: string + messages: { + /** + * T​u​n​n​e​l​ ​e​d​i​t​e​d + */ + editSuccess: string + /** + * E​d​i​t​i​n​g​ ​t​u​n​n​e​l​ ​f​a​i​l​e​d + */ + editError: string + } + controls: { + /** + * S​a​v​e​ ​c​h​a​n​g​e​s + */ + save: string + } } addTunnelPage: { /** @@ -1609,6 +1637,34 @@ export type TranslationFunctions = { * WireGuard Tunnels */ title: () => LocalizedString + header: { + /** + * Edit Tunnel + */ + edit: () => LocalizedString + } + } + editTunnelPage: { + /** + * Edit WireGuard® Tunnel + */ + title: () => LocalizedString + messages: { + /** + * Tunnel edited + */ + editSuccess: () => LocalizedString + /** + * Editing tunnel failed + */ + editError: () => LocalizedString + } + controls: { + /** + * Save changes + */ + save: () => LocalizedString + } } addTunnelPage: { /** diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index 4d448c35..8cebd845 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -8,6 +8,7 @@ import { Connection, DefguardInstance, LocationStats, + Tunnel, } from '../types'; import { ConnectionRequest, @@ -99,6 +100,9 @@ const getLocationDetails = async ( const getTunnels = async (): Promise => invokeWrapper('all_tunnels'); +const getTunnelDetails = async (id: number): Promise => + invokeWrapper('tunnel_details', { tunnelId: id }); + export const clientApi = { getInstances, getTunnels, @@ -118,4 +122,5 @@ export const clientApi = { updateInstance, parseTunnelConfig, saveTunnel, + getTunnelDetails, }; diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 0e0f30b6..0d68075c 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -122,4 +122,5 @@ export type TauriCommandKey = | 'parse_tunnel_config' | 'save_tunnel' | 'all_tunnels' + | 'tunnel_details' | 'location_interface_details'; diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index 36b6c81e..d684c23a 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -9,6 +9,7 @@ import SvgDefguardLogoIcon from '../../../../shared/components/svg/DefguardLogoI import SvgDefguardLogoText from '../../../../shared/components/svg/DefguardLogoText'; import SvgIconNavConnections from '../../../../shared/components/svg/IconNavConnections'; import SvgIconNavVpn from '../../../../shared/components/svg/IconNavVpn'; +import { Divider } from '../../../../shared/defguard-ui/components/Layout/Divider/Divider'; import { IconContainer } from '../../../../shared/defguard-ui/components/Layout/IconContainer/IconContainer'; import SvgIconPlus from '../../../../shared/defguard-ui/components/svg/IconPlus'; import SvgIconSettings from '../../../../shared/defguard-ui/components/svg/IconSettings'; @@ -20,11 +21,12 @@ import { ClientBarItem } from './components/ClientBarItem/ClientBarItem'; export const ClientSideBar = () => { const navigate = useNavigate(); const { LL } = useI18nContext(); - const [instances, tunnels, setClientStore] = useClientStore((state) => [ - state.instances, - state.tunnels, - state.setState, - ]); + const [selectedInstance, instances, tunnels, setClientStore] = useClientStore( + (state) => [state.selectedInstance, state.instances, state.tunnels, state.setState], + ); + const tunnelPathActive = + selectedInstance?.id === undefined && + selectedInstance?.type === WireguardInstanceType.TUNNEL; return (
@@ -47,8 +49,11 @@ export const ClientSideBar = () => { /> ))} +
{ setClientStore({ diff --git a/src/pages/client/components/ClientSideBar/style.scss b/src/pages/client/components/ClientSideBar/style.scss index 56313b5f..ddb3c26a 100644 --- a/src/pages/client/components/ClientSideBar/style.scss +++ b/src/pages/client/components/ClientSideBar/style.scss @@ -76,6 +76,7 @@ @media (min-height: 600px) { padding-top: 70px; } + & > .client-bar-item { display: grid; box-sizing: border-box; diff --git a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx index 11b75836..06fa805a 100644 --- a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx +++ b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx @@ -1,6 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useEffect, useMemo, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; import { z } from 'zod'; import { useI18nContext } from '../../../../../../i18n/i18n-react'; @@ -24,6 +25,7 @@ import { patternValidIp, patternValidWireguardKey, } from '../../../../../../shared/patterns'; +import { routes } from '../../../../../../shared/routes'; import { generateWGKeys } from '../../../../../../shared/utils/generateWGKeys'; import { validateIpOrDomainList } from '../../../../../../shared/validators/tunnel'; import { clientApi } from '../../../../clientAPI/clientApi'; @@ -65,6 +67,7 @@ export const AddTunnelFormCard = () => { const { LL } = useI18nContext(); const { parseTunnelConfig, saveTunnel } = clientApi; const toaster = useToaster(); + const navigate = useNavigate(); const localLL = LL.pages.client.pages.addTunnelPage.forms.initTunnel; /* eslint-disable no-useless-escape */ @@ -102,21 +105,37 @@ export const AddTunnelFormCard = () => { .refine((value) => { return patternValidEndpoint.test(value); }, LL.form.errors.invalid()), - dns: z.string().refine((value) => { - return validateIpOrDomainList(value, ',', true); - }, LL.form.errors.invalid()), + dns: z + .string() + .refine((value) => { + if (value) { + return validateIpOrDomainList(value, ',', true); + } + return true; + }, LL.form.errors.invalid()) + .optional(), allowed_ips: z.string().refine((value) => { - const ips = value.split(',').map((ip) => ip.trim()); - return ips.every((ip) => cidrRegex.test(ip)); + if (value) { + const ips = value.split(',').map((ip) => ip.trim()); + return ips.every((ip) => cidrRegex.test(ip)); + } + return true; }, LL.form.errors.invalid()), persistent_keep_alive: z.number(), route_all_traffic: z.boolean(), + pre_up: z.string().nullable(), + post_up: z.string().nullable(), + pre_down: z.string().nullable(), + post_down: z.string().nullable(), }), [LL.form.errors], ); const handleValidSubmit: SubmitHandler = (values) => { saveTunnel(values) - .then(() => toaster.success(localLL.messages.addSuccess())) + .then(() => { + navigate(routes.client.base, { replace: true }); + toaster.success(localLL.messages.addSuccess()); + }) .catch(() => toaster.error(localLL.messages.addError())); }; const { handleSubmit, control, reset, setValue } = useForm({ diff --git a/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx b/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx new file mode 100644 index 00000000..7b40f04d --- /dev/null +++ b/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx @@ -0,0 +1,69 @@ +import './style.scss'; + +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useI18nContext } from '../../../../i18n/i18n-react'; +import SvgIconCheckmarkSmall from '../../../../shared/components/svg/IconCheckmarkSmall'; +import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../shared/defguard-ui/components/Layout/Button/types'; +import { routes } from '../../../../shared/routes'; +import { clientApi } from '../../clientAPI/clientApi'; +import { useClientStore } from '../../hooks/useClientStore'; +import { clientQueryKeys } from '../../query'; +import { WireguardInstanceType } from '../../types'; +import { EditTunnelFormCard } from './components/EditTunnelFormCard'; + +const { getTunnelDetails } = clientApi; + +export const ClientEditTunnelPage = () => { + const { LL } = useI18nContext(); + const navigate = useNavigate(); + const submitRef = useRef(null); + const selectedInstance = useClientStore((state) => state.selectedInstance); + useEffect(() => { + if ( + selectedInstance?.id === undefined || + selectedInstance.type !== WireguardInstanceType.TUNNEL + ) { + navigate(routes.client.base, { replace: true }); + } + }, [selectedInstance, navigate]); + + const { data: tunnel } = useQuery({ + queryKey: [clientQueryKeys.getTunnels, selectedInstance?.id as number], + queryFn: () => getTunnelDetails(selectedInstance?.id as number), + enabled: !!selectedInstance?.id, + }); + return ( +
+
+

{LL.pages.client.pages.editTunnelPage.title()}

+
+
+
+
+ {tunnel && } +
+
+ ); +}; diff --git a/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx b/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx new file mode 100644 index 00000000..2becd1aa --- /dev/null +++ b/src/pages/client/pages/ClientEditTunnelPage/components/EditTunnelFormCard.tsx @@ -0,0 +1,298 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMemo, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; +import { z } from 'zod'; + +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { FormInput } from '../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; +import { ArrowSingle } from '../../../../../shared/defguard-ui/components/icons/ArrowSingle/ArrowSingle'; +import { + ArrowSingleDirection, + ArrowSingleSize, +} from '../../../../../shared/defguard-ui/components/icons/ArrowSingle/types'; +import { Card } from '../../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; +import { useToaster } from '../../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import { + cidrRegex, + patternValidEndpoint, + patternValidIp, + patternValidWireguardKey, +} from '../../../../../shared/patterns'; +import { routes } from '../../../../../shared/routes'; +import { validateIpOrDomainList } from '../../../../../shared/validators/tunnel'; +import { clientApi } from '../../../clientAPI/clientApi'; +import { Tunnel } from '../../../types'; + +type Props = { + tunnel: Tunnel; + submitRef: React.MutableRefObject; // Add submitRef prop +}; + +type FormFields = { + id?: number; + name: string; + pubkey: string; + prvkey: string; + address: string; + server_pubkey: string; + allowed_ips?: string; + endpoint: string; + dns?: string; + persistent_keep_alive: number; + route_all_traffic: boolean; + pre_up?: string; + post_up?: string; + pre_down?: string; + post_down?: string; +}; +const defaultValues: FormFields = { + name: '', + pubkey: '', + prvkey: '', + address: '', + server_pubkey: '', + allowed_ips: '', + endpoint: '', + dns: '', + persistent_keep_alive: 25, // Adjust as needed + route_all_traffic: false, + pre_up: '', + post_up: '', + pre_down: '', + post_down: '', +}; +const { saveTunnel } = clientApi; + +const tunnelToForm = (tunnel: Tunnel): FormFields => { + const { + id, + pubkey, + prvkey, + server_pubkey, + allowed_ips, + dns, + persistent_keep_alive, + pre_up, + post_up, + pre_down, + post_down, + ...commonFields + } = tunnel; + + return { + id: id, + pubkey, + prvkey, + server_pubkey, + allowed_ips: allowed_ips || '', + dns: dns || '', + persistent_keep_alive, + pre_up: pre_up || '', + post_up: post_up || '', + pre_down: pre_down || '', + post_down: post_down || '', + ...commonFields, + }; +}; + +export const EditTunnelFormCard = ({ tunnel, submitRef }: Props) => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.addTunnelPage.forms.initTunnel; + const navigate = useNavigate(); + const toaster = useToaster(); + + const defaultFormValues: FormFields = useMemo(() => { + if (tunnel) { + return tunnelToForm(tunnel); + } + return defaultValues; + }, [tunnel]); + + const schema = useMemo( + () => + z.object({ + id: z.number(), + name: z.string().trim().min(1, LL.form.errors.required()), + pubkey: z + .string() + .trim() + .min(1, LL.form.errors.required()) + .refine((value) => { + return patternValidWireguardKey.test(value); + }, LL.form.errors.invalid()), + prvkey: z + .string() + .trim() + .min(1, LL.form.errors.required()) + .refine((value) => { + return patternValidWireguardKey.test(value); + }, LL.form.errors.invalid()), + server_pubkey: z + .string() + .trim() + .min(1, LL.form.errors.required()) + .refine((value) => { + return patternValidWireguardKey.test(value); + }, LL.form.errors.invalid()), + address: z.string().refine((value) => { + return patternValidIp.test(value); + }, LL.form.errors.invalid()), + endpoint: z + .string() + .min(1, LL.form.errors.required()) + .refine((value) => { + return patternValidEndpoint.test(value); + }, LL.form.errors.invalid()), + dns: z + .string() + .refine((value) => { + if (value) { + return validateIpOrDomainList(value, ',', true); + } + return true; + }, LL.form.errors.invalid()) + .optional(), + allowed_ips: z.string().refine((value) => { + if (value) { + const ips = value.split(',').map((ip) => ip.trim()); + return ips.every((ip) => cidrRegex.test(ip)); + } + return true; + }, LL.form.errors.invalid()), + persistent_keep_alive: z.number(), + route_all_traffic: z.boolean(), + pre_up: z.string().nullable(), + post_up: z.string().nullable(), + pre_down: z.string().nullable(), + post_down: z.string().nullable(), + }), + [LL.form.errors], + ); + + const handleValidSubmit: SubmitHandler = (values) => { + saveTunnel(values) + .then(() => { + navigate(routes.client.base, { replace: true }); + toaster.success(LL.pages.client.pages.editTunnelPage.messages.editSuccess()); + }) + .catch(() => + toaster.error(LL.pages.client.pages.editTunnelPage.messages.editError()), + ); + }; + + const { handleSubmit, control } = useForm({ + resolver: zodResolver(schema), + defaultValues: defaultFormValues, + mode: 'all', + }); + + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + const handleToggleAdvancedOptions = () => { + setShowAdvancedOptions(!showAdvancedOptions); + }; + + return ( + <> +
+ +
+

Tunnel Configuration

+
+
+
+ {localLL.helpers.name()}} + /> + {localLL.helpers.prvkey()}} + /> + {localLL.helpers.pubkey()}} + /> + {localLL.helpers.address()}} + /> +
+
+ +

{localLL.sections.vpnServer()}

+ {localLL.helpers.serverPubkey()}} + /> + {localLL.helpers.endpoint()}} + /> + {localLL.helpers.dns()}} + /> + {localLL.helpers.allowedIps()}} + /> + + {localLL.helpers.persistentKeepAlive()}} + /> +
+

{localLL.sections.advancedOptions()}

+ {localLL.helpers.advancedOptions()} +
+ +
+
+ {localLL.helpers.preUp()}} + /> + {localLL.helpers.postUp()}} + /> + {localLL.helpers.preDown()}} + /> + {localLL.helpers.postDown()}} + /> +
+
+ +
+ + ); +}; diff --git a/src/pages/client/pages/ClientEditTunnelPage/style.scss b/src/pages/client/pages/ClientEditTunnelPage/style.scss new file mode 100644 index 00000000..977aa191 --- /dev/null +++ b/src/pages/client/pages/ClientEditTunnelPage/style.scss @@ -0,0 +1,109 @@ +@use '@scssutils' as *; + +#client-edit-tunnel-page { + h1 { + @include typography(app-title); + color: var(--text-body-primary); + } + + h2 { + @include typography(app-body-1); + color: var(--text-body-primary); + } + h3 { + @include typography(app-side-bar); + color: var(--text-body-primary); + } + + & > header { + width: 100%; + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + padding-bottom: 15px; + + & > h1 { + text-align: left; + } + & > .controls { + margin-left: auto; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + gap: 20px; + .btn { + width: 100%; + min-width: 130px; + } + } + } + + & > .content { + form { + & > * { + width: 100%; + } + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 50px; + align-items: flex-start; + @include media-breakpoint-down(xxl) { + justify-content: flex-start; + flex-direction: column; + } + + & > .card { + box-sizing: border-box; + padding: 32px 64px; + } + + & > .client { + border-bottom: 1px solid var(--border-primary); + margin-bottom: 10px; + } + & > h3 { + margin-bottom: 10px; + } + .advanced-options-header { + display: flex; + align-items: center; + gap: 5px; + margin-bottom: 10px; + & > button { + background: none; + border: none; + padding: 0; + margin: 0; + } + .arrow-single { + width: 22px; + height: 22px; + margin-left: auto; + } + .underscore { + flex-grow: 1; + border-bottom: 1px solid var(--border-primary); + margin-right: 10px; + } + } + .advanced-options { + display: none; + transition: opacity 0.5s ease; + } + + .advanced-options.open { + display: block; + } + > .controls { + padding-top: 42px; + + .btn { + height: 47px; + } + } + } + } +} diff --git a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx index beadadba..cc8c0db0 100644 --- a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx +++ b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx @@ -31,7 +31,7 @@ export const ClientInstancePage = () => { ); const navigate = useNavigate(); - const isLocationPage = selectedInstanceType === WireguardInstanceType.TUNNEL; + const isLocationPage = selectedInstanceType === WireguardInstanceType.DEFGUARD_INSTANCE; const openUpdateInstanceModal = useUpdateInstanceModal((state) => state.open); @@ -45,10 +45,10 @@ export const ClientInstancePage = () => { return (
-

{!isLocationPage ? instanceLL.title() : tunelLL.title()}

+

{isLocationPage ? instanceLL.title() : tunelLL.title()}

- {!isLocationPage && ( + {isLocationPage && ( <>
diff --git a/src/shared/routes.ts b/src/shared/routes.ts index 868d0535..85e6af8a 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -6,6 +6,7 @@ export const routes = { instancePage: '/client/', addInstance: '/client/add-instance', addTunnel: '/client/add-tunnel', + editTunnel: '/client/edit-tunnel', settings: '/client/settings', }, enrollment: '/enrollment', From 2fdd531f28b48a3f4f02f1cb27bc7cdce144a3ab Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Thu, 4 Jan 2024 09:53:41 +0100 Subject: [PATCH 19/45] feat: Execute Tunnel PrePostUpDown commands (#143) * execute PrePostUpDown commands in daemon * update proto * execute command after closing app * add function to remove interface --------- Co-authored-by: akantorczyk --- src-tauri/proto | 2 +- src-tauri/src/appstate.rs | 37 ++------------- src-tauri/src/commands.rs | 33 +++---------- src-tauri/src/service/mod.rs | 24 +++++++++- src-tauri/src/utils.rs | 89 ++++++++++++++++++++++++++++++++++-- 5 files changed, 119 insertions(+), 66 deletions(-) diff --git a/src-tauri/proto b/src-tauri/proto index 9f5c9026..6515f45c 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit 9f5c90266c9d3449c38197b72e8a9d8a56f12cfd +Subproject commit 6515f45cd8284fa2f727e32dfe056dc9b13b1e4e diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index 8b975fdb..97882d62 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -7,14 +7,11 @@ use tokio_util::sync::CancellationToken; use tonic::transport::Channel; use crate::{ - database::{ActiveConnection, Connection, DbPool, TunnelConnection}, - error::Error, + database::{ActiveConnection, DbPool}, service::{ - proto::{ - desktop_daemon_service_client::DesktopDaemonServiceClient, RemoveInterfaceRequest, - }, - utils::setup_client, + proto::desktop_daemon_service_client::DesktopDaemonServiceClient, utils::setup_client, }, + utils::disconnect_interface, ConnectionType, }; @@ -100,33 +97,7 @@ impl AppState { debug!("Found active connection"); trace!("Connection: {connection:#?}"); debug!("Removing interface"); - let mut client = self.client.clone(); - let request = RemoveInterfaceRequest { - interface_name: connection.interface_name.clone(), - }; - if let Err(error) = client.remove_interface(request).await { - error!("Failed to remove interface: {error}"); - return Err(Error::InternalError); - } - debug!("Removed interface"); - debug!("Saving connection"); - trace!("Connection: {connection:#?}"); - match connection.connection_type { - ConnectionType::Location => { - let mut connection: Connection = connection.into(); - connection.save(&self.get_pool()).await?; - debug!("Connection saved"); - trace!("Saved connection: {connection:#?}"); - info!("Location {} disconnected", connection.location_id); - } - ConnectionType::Tunnel => { - let mut connection: TunnelConnection = connection.into(); - connection.save(&self.get_pool()).await?; - debug!("Connection saved"); - trace!("Saved connection: {connection:#?}"); - info!("Tunnel {} disconnected", connection.tunnel_id); - } - } + disconnect_interface(connection, self).await?; } Ok(()) } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 6d777560..d3f638d6 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -10,7 +10,7 @@ use crate::{ service::{log_watcher::stop_log_watcher_task, proto::RemoveInterfaceRequest}, tray::configure_tray_icon, utils::{ - get_location_interface_details, get_tunnel_interface_details, + disconnect_interface, get_location_interface_details, get_tunnel_interface_details, handle_connection_for_location, handle_connection_for_tunnel, }, wg_config::parse_wireguard_config, @@ -59,32 +59,11 @@ pub async fn disconnect( ) -> Result<(), Error> { debug!("Disconnecting location {}", location_id); let state = handle.state::(); - if let Some(connection) = state.find_and_remove_connection(location_id, &connection_type) { - debug!("Found active connection"); - trace!("Connection: {:#?}", connection); - debug!("Removing interface"); - let mut client = state.client.clone(); let interface_name = connection.interface_name.clone(); - let request = RemoveInterfaceRequest { - interface_name: interface_name.clone(), - }; - if let Err(error) = client.remove_interface(request).await { - error!("Failed to remove interface: {error}"); - return Err(Error::InternalError); - } - debug!("Removed interface"); - debug!("Saving connection"); + debug!("Found active connection"); trace!("Connection: {:#?}", connection); - if connection_type.eq(&ConnectionType::Location) { - let mut connection: Connection = connection.into(); - connection.save(&state.get_pool()).await?; - trace!("Saved connection: {connection:#?}"); - } else { - let mut connection: TunnelConnection = connection.into(); - connection.save(&state.get_pool()).await?; - trace!("Saved connection: {connection:#?}"); - } + disconnect_interface(connection, &state).await?; debug!("Connection saved"); handle.emit_all( "connection-changed", @@ -92,16 +71,14 @@ pub async fn disconnect( message: "Created new connection".into(), }, )?; - stop_log_watcher_task(handle, interface_name)?; - - info!("Location {location_id} {connection_type:?} disconnected"); Ok(()) } else { error!("Connection for location with id: {location_id} not found"); Err(Error::NotFound) } } + #[derive(Debug, Serialize, Deserialize)] pub struct Device { pub id: i64, @@ -600,6 +577,8 @@ pub async fn delete_instance(instance_id: i64, handle: AppHandle) -> Result<(), debug!("Found active connection for location({location_id}), closing...",); let request = RemoveInterfaceRequest { interface_name: connection.interface_name.clone(), + pre_down: None, + post_down: None, }; client .remove_interface(request) diff --git a/src-tauri/src/service/mod.rs b/src-tauri/src/service/mod.rs index 177dbeed..476a0bc7 100644 --- a/src-tauri/src/service/mod.rs +++ b/src-tauri/src/service/mod.rs @@ -26,7 +26,7 @@ use tonic::{ use tracing::{debug, info, info_span}; use self::config::Config; -use crate::utils::IS_MACOS; +use crate::utils::{execute_command, IS_MACOS}; use proto::{ desktop_daemon_service_server::{DesktopDaemonService, DesktopDaemonServiceServer}, @@ -91,6 +91,12 @@ impl DesktopDaemonService for DaemonService { // setup WireGuard API let wgapi = setup_wgapi(ifname.clone())?; + if let Some(pre_up) = request.pre_up { + debug!("Executing specified PreUp command: {pre_up}"); + let _ = execute_command(&pre_up); + info!("Executed specified PreUp command: {pre_up}"); + } + // create new interface debug!("Creating new interface {ifname}"); wgapi.create_interface().map_err(|err| { @@ -129,6 +135,11 @@ impl DesktopDaemonService for DaemonService { error!("{msg}"); Status::new(Code::Internal, msg) })?; + if let Some(post_up) = request.post_up { + debug!("Executing specified PostUp command: {post_up}"); + let _ = execute_command(&post_up); + info!("Executed specified PostUp command: {post_up}"); + } Ok(Response::new(())) } @@ -143,13 +154,22 @@ impl DesktopDaemonService for DaemonService { info!("Removing interface {ifname}"); // setup WireGuard API let wgapi = setup_wgapi(ifname.clone())?; - + if let Some(pre_down) = request.pre_down { + debug!("Executing specified PreDown command: {pre_down}"); + let _ = execute_command(&pre_down); + info!("Executed specified PreDown command: {pre_down}"); + } // remove interface wgapi.remove_interface().map_err(|err| { let msg = format!("Failed to remove WireGuard interface {ifname}: {err}"); error!("{msg}"); Status::new(Code::Internal, msg) })?; + if let Some(post_down) = request.post_down { + debug!("Executing specified PostDown command: {post_down}"); + let _ = execute_command(&post_down); + info!("Executed specified PostDown command: {post_down}"); + } Ok(Response::new(())) } diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 8a2cc595..76c4e413 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,6 +1,7 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, path::PathBuf, + process::Command, str::FromStr, }; use tauri::AppHandle; @@ -15,14 +16,14 @@ use crate::{ commands::{LocationInterfaceDetails, Payload}, database::{ models::location::peer_to_location_stats, models::tunnel::peer_to_tunnel_stats, - ActiveConnection, DbPool, Location, Tunnel, WireguardKeys, + ActiveConnection, Connection, DbPool, Location, Tunnel, TunnelConnection, WireguardKeys, }, error::Error, service::{ log_watcher::spawn_log_watcher_task, proto::{ desktop_daemon_service_client::DesktopDaemonServiceClient, CreateInterfaceRequest, - ReadInterfaceDataRequest, + ReadInterfaceDataRequest, RemoveInterfaceRequest, }, }, ConnectionType, @@ -92,6 +93,8 @@ pub async fn setup_interface( config: Some(interface_config.clone().into()), allowed_ips, dns: location.dns.clone(), + pre_up: None, + post_up: None, }; if let Err(error) = client.create_interface(request).await { error!("Failed to create interface: {error}"); @@ -292,7 +295,7 @@ pub async fn setup_interface_tunnel( .unwrap_or_default() }; for allowed_ip in &allowed_ips { - match IpAddrMask::from_str(allowed_ip) { + match IpAddrMask::from_str(allowed_ip.trim()) { Ok(addr) => { peer.allowed_ips.push(addr); } @@ -319,6 +322,8 @@ pub async fn setup_interface_tunnel( config: Some(interface_config.clone().into()), allowed_ips, dns: tunnel.dns.clone(), + pre_up: tunnel.pre_up.clone(), + post_up: tunnel.post_up.clone(), }; if let Err(error) = client.create_interface(request).await { error!("Failed to create interface: {error}"); @@ -575,3 +580,81 @@ pub async fn handle_connection_for_tunnel(tunnel: &Tunnel, handle: AppHandle) -> .await?; Ok(()) } +/// Execute command passed as argument. +pub fn execute_command(command: &str) -> Result<(), Error> { + let mut command_parts = command.split_whitespace(); + + if let Some(command) = command_parts.next() { + let output = Command::new(command).args(command_parts).output()?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + info!("Command executed successfully. Stdout:\n{}", stdout); + if !stderr.is_empty() { + error!("Stderr:\n{stderr}"); + } + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + error!("Error executing command. Stderr:\n{stderr}"); + } + } + Ok(()) +} +/// Helper function to remove interface and close connection +pub async fn disconnect_interface( + active_connection: ActiveConnection, + state: &AppState, +) -> Result<(), Error> { + debug!("Removing interface"); + let mut client = state.client.clone(); + let interface_name = active_connection.interface_name.clone(); + let (id, connection_type) = ( + active_connection.location_id, + active_connection.connection_type.clone(), + ); + match active_connection.connection_type { + ConnectionType::Location => { + let request = RemoveInterfaceRequest { + interface_name: interface_name.clone(), + pre_down: None, + post_down: None, + }; + if let Err(error) = client.remove_interface(request).await { + error!("Failed to remove interface: {error}"); + return Err(Error::InternalError); + } + let mut connection: Connection = active_connection.into(); + connection.save(&state.get_pool()).await?; + trace!("Saved connection: {connection:#?}"); + debug!("Removed interface"); + debug!("Saving connection"); + trace!("Connection: {:#?}", connection); + } + ConnectionType::Tunnel => { + if let Some(tunnel) = + Tunnel::find_by_id(&state.get_pool(), active_connection.location_id).await? + { + let request = RemoveInterfaceRequest { + interface_name: interface_name.clone(), + pre_down: tunnel.pre_down, + post_down: tunnel.post_down, + }; + if let Err(error) = client.remove_interface(request).await { + error!("Failed to remove interface: {error}"); + return Err(Error::InternalError); + } + let mut connection: TunnelConnection = active_connection.into(); + connection.save(&state.get_pool()).await?; + trace!("Saved connection: {connection:#?}"); + } else { + error!("Tunnel with ID {} not found", active_connection.location_id); + return Err(Error::NotFound); + } + } + } + + info!("Location {} {:?} disconnected", id, connection_type); + Ok(()) +} From a780f9fea424bb8a5cfde31477688ec62e45a790 Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Thu, 4 Jan 2024 10:10:19 +0100 Subject: [PATCH 20/45] style: navbar update --- .../client/components/ClientSideBar/ClientSideBar.tsx | 6 ++++-- src/pages/client/components/ClientSideBar/style.scss | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index d684c23a..cc13ceae 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -37,7 +37,7 @@ export const ClientSideBar = () => {
-
+

{LL.pages.client.sideBar.instances()}

@@ -49,7 +49,9 @@ export const ClientSideBar = () => { /> ))} - +
+ +
.items { display: flex; flex-grow: 1; + height: 45vh; flex-shrink: 0; flex-flow: column; align-items: flex-start; justify-content: flex-start; box-sizing: border-box; row-gap: 15px; + &.flex-end { + justify-content: flex-end; + @media (min-height: 600px) { + padding-bottom: 70px; + } + } @include media-breakpoint-up(lg) { row-gap: 0; From cff80330f8ea5236ad8da71dc35dda773da185c3 Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Thu, 4 Jan 2024 12:10:41 +0100 Subject: [PATCH 21/45] feat: Tunnel/Instance created page (#146) * feat: Tunnel/Instance Added page --- src/components/App/App.tsx | 10 +++ src/i18n/en/index.ts | 18 +++++ src/i18n/i18n-types.ts | 68 +++++++++++++++++++ .../AddInstanceDeviceForm.tsx | 2 +- .../AddTunnelFormCard/AddTunnelFormCard.tsx | 2 +- .../pages/ClientAddedPage/ClientAddedPage.tsx | 48 +++++++++++++ .../client/pages/ClientAddedPage/style.scss | 45 ++++++++++++ src/shared/routes.ts | 2 + 8 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/pages/client/pages/ClientAddedPage/ClientAddedPage.tsx create mode 100644 src/pages/client/pages/ClientAddedPage/style.scss diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 5e364910..1b5d3c2b 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -23,11 +23,13 @@ import { loadLocaleAsync } from '../../i18n/i18n-util.async'; import { clientApi } from '../../pages/client/clientAPI/clientApi'; import { ClientPage } from '../../pages/client/ClientPage'; import { useClientStore } from '../../pages/client/hooks/useClientStore'; +import { ClientAddedPage } from '../../pages/client/pages/ClientAddedPage/ClientAddedPage'; import { ClientAddInstancePage } from '../../pages/client/pages/ClientAddInstancePage/ClientAddInstnacePage'; import { ClientAddTunnelPage } from '../../pages/client/pages/ClientAddTunnelPage/ClientAddTunnelPage'; import { ClientEditTunnelPage } from '../../pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage'; import { ClientInstancePage } from '../../pages/client/pages/ClientInstancePage/ClientInstancePage'; import { ClientSettingsPage } from '../../pages/client/pages/ClientSettingsPage/ClientSettingsPage'; +import { WireguardInstanceType } from '../../pages/client/types'; import { EnrollmentPage } from '../../pages/enrollment/EnrollmentPage'; import { SessionTimeoutPage } from '../../pages/sessionTimeout/SessionTimeoutPage'; import { ToastManager } from '../../shared/defguard-ui/components/Layout/ToastManager/ToastManager'; @@ -72,10 +74,18 @@ const router = createBrowserRouter([ path: '/client/add-instance', element: , }, + { + path: '/client/instance-created', + element: , + }, { path: '/client/add-tunnel', element: , }, + { + path: '/client/tunnel-created', + element: , + }, { path: '/client/edit-tunnel', element: , diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 9badd2d8..31a51aa9 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -85,6 +85,24 @@ const en = { }, }, }, + createdPage: { + tunnel: { + title: 'Your Tunnel Was Added Successfully', + content: + 'Your tunnel has been successfully added. You can now connect this device, check its status and view statistics using the menu in the left sidebar.', + controls: { + submit: 'Add Another Tunnel', + }, + }, + instance: { + title: 'Your Instance Was Added Successfully', + content: + 'Your instance has been successfully added. You can now connect this device, check its status and view statistics using the menu in the left sidebar.', + controls: { + submit: 'Add Another Instance', + }, + }, + }, instancePage: { title: 'Locations', controls: { diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 916b90c1..4620fa4f 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -214,6 +214,40 @@ type RootTranslation = { } } } + createdPage: { + tunnel: { + /** + * Y​o​u​r​ ​T​u​n​n​e​l​ ​W​a​s​ ​A​d​d​e​d​ ​S​u​c​c​e​s​s​f​u​l​l​y + */ + title: string + /** + * Y​o​u​r​ ​t​u​n​n​e​l​ ​h​a​s​ ​b​e​e​n​ ​s​u​c​c​e​s​s​f​u​l​l​y​ ​a​d​d​e​d​.​ ​Y​o​u​ ​c​a​n​ ​n​o​w​ ​c​o​n​n​e​c​t​ ​t​h​i​s​ ​d​e​v​i​c​e​,​ ​c​h​e​c​k​ ​i​t​s​ ​s​t​a​t​u​s​ ​a​n​d​ ​v​i​e​w​ ​s​t​a​t​i​s​t​i​c​s​ ​u​s​i​n​g​ ​t​h​e​ ​m​e​n​u​ ​i​n​ ​t​h​e​ ​l​e​f​t​ ​s​i​d​e​b​a​r​. + */ + content: string + controls: { + /** + * A​d​d​ ​A​n​o​t​h​e​r​ ​T​u​n​n​e​l + */ + submit: string + } + } + instance: { + /** + * Y​o​u​r​ ​I​n​s​t​a​n​c​e​ ​W​a​s​ ​A​d​d​e​d​ ​S​u​c​c​e​s​s​f​u​l​l​y + */ + title: string + /** + * Y​o​u​r​ ​i​n​s​t​a​n​c​e​ ​h​a​s​ ​b​e​e​n​ ​s​u​c​c​e​s​s​f​u​l​l​y​ ​a​d​d​e​d​.​ ​Y​o​u​ ​c​a​n​ ​n​o​w​ ​c​o​n​n​e​c​t​ ​t​h​i​s​ ​d​e​v​i​c​e​,​ ​c​h​e​c​k​ ​i​t​s​ ​s​t​a​t​u​s​ ​a​n​d​ ​v​i​e​w​ ​s​t​a​t​i​s​t​i​c​s​ ​u​s​i​n​g​ ​t​h​e​ ​m​e​n​u​ ​i​n​ ​t​h​e​ ​l​e​f​t​ ​s​i​d​e​b​a​r​. + */ + content: string + controls: { + /** + * A​d​d​ ​A​n​o​t​h​e​r​ ​I​n​s​t​a​n​c​e + */ + submit: string + } + } + } instancePage: { /** * L​o​c​a​t​i​o​n​s @@ -1430,6 +1464,40 @@ export type TranslationFunctions = { } } } + createdPage: { + tunnel: { + /** + * Your Tunnel Was Added Successfully + */ + title: () => LocalizedString + /** + * Your tunnel has been successfully added. You can now connect this device, check its status and view statistics using the menu in the left sidebar. + */ + content: () => LocalizedString + controls: { + /** + * Add Another Tunnel + */ + submit: () => LocalizedString + } + } + instance: { + /** + * Your Instance Was Added Successfully + */ + title: () => LocalizedString + /** + * Your instance has been successfully added. You can now connect this device, check its status and view statistics using the menu in the left sidebar. + */ + content: () => LocalizedString + controls: { + /** + * Add Another Instance + */ + submit: () => LocalizedString + } + } + } instancePage: { /** * Locations diff --git a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx index 865709c1..cd6d9a83 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx @@ -109,7 +109,7 @@ export const AddInstanceDeviceForm = ({ response }: Props) => { type: WireguardInstanceType.DEFGUARD_INSTANCE, }, }); - navigate(routes.client.base, { replace: true }); + navigate(routes.client.instanceCreated, { replace: true }); }) .catch(() => { toaster.error(LL.common.messages.error()); diff --git a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx index 06fa805a..bf01a225 100644 --- a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx +++ b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx @@ -133,7 +133,7 @@ export const AddTunnelFormCard = () => { const handleValidSubmit: SubmitHandler = (values) => { saveTunnel(values) .then(() => { - navigate(routes.client.base, { replace: true }); + navigate(routes.client.tunnelCreated, { replace: true }); toaster.success(localLL.messages.addSuccess()); }) .catch(() => toaster.error(localLL.messages.addError())); diff --git a/src/pages/client/pages/ClientAddedPage/ClientAddedPage.tsx b/src/pages/client/pages/ClientAddedPage/ClientAddedPage.tsx new file mode 100644 index 00000000..51792cca --- /dev/null +++ b/src/pages/client/pages/ClientAddedPage/ClientAddedPage.tsx @@ -0,0 +1,48 @@ +import './style.scss'; + +import { useNavigate } from 'react-router-dom'; + +import { useI18nContext } from '../../../../i18n/i18n-react'; +import SvgVpnLocation from '../../../../shared/components/svg/VpnLocation'; +import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../shared/defguard-ui/components/Layout/Button/types'; +import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { routes } from '../../../../shared/routes'; +import { WireguardInstanceType } from '../../types'; + +type Props = { + pageType: WireguardInstanceType; +}; + +export const ClientAddedPage = ({ pageType }: Props) => { + const { LL } = useI18nContext(); + const navigate = useNavigate(); + const [localLL, navigateRoute] = + pageType === WireguardInstanceType.TUNNEL + ? [LL.pages.client.pages.createdPage.tunnel, routes.client.addTunnel] + : [LL.pages.client.pages.createdPage.instance, routes.client.addInstance]; + + return ( +
+
+ +
+

{localLL.title()}

+ +

{localLL.content()}

+
+
+
+
+ ); +}; diff --git a/src/pages/client/pages/ClientAddedPage/style.scss b/src/pages/client/pages/ClientAddedPage/style.scss new file mode 100644 index 00000000..4ad6c3b5 --- /dev/null +++ b/src/pages/client/pages/ClientAddedPage/style.scss @@ -0,0 +1,45 @@ +@use '@scssutils' as *; + +#created-page { + h2 { + @include typography(app-body-1); + } + display: flex; + justify-content: center; + align-items: center; + overflow-x: auto; + + & > .content { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; + + @include media-breakpoint-up(xxl) { + justify-content: flex-start; + } + + & > .card { + box-sizing: border-box; + padding: 32px 64px; + max-width: 700px; + min-width: 300px; + display: flex; + & > .card-content { + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: column; + gap: 50px; + & > p { + @include typography(app-body-2); + text-align: center; + } + & > button { + width: 260px; + height: 50px; + } + } + } + } +} diff --git a/src/shared/routes.ts b/src/shared/routes.ts index 85e6af8a..a3a4ef36 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -6,6 +6,8 @@ export const routes = { instancePage: '/client/', addInstance: '/client/add-instance', addTunnel: '/client/add-tunnel', + tunnelCreated: '/client/tunnel-created', + instanceCreated: '/client/instance-created', editTunnel: '/client/edit-tunnel', settings: '/client/settings', }, From 3ea3dc61482e6e40ba70755fc294c73a5af3be52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Thu, 4 Jan 2024 13:08:46 +0100 Subject: [PATCH 22/45] feat: welcome carousel (#147) --- src-tauri/Cargo.lock | 102 +++++++- src-tauri/Cargo.toml | 6 +- src-tauri/src/bin/defguard-client.rs | 7 +- src-tauri/src/commands.rs | 8 + src-tauri/src/error.rs | 2 + src/components/App/App.tsx | 5 + src/i18n/en/index.ts | 56 +++++ src/i18n/i18n-types.ts | 180 ++++++++++++++ src/pages/client/ClientPage.tsx | 14 +- src/pages/client/clientAPI/clientApi.ts | 5 + src/pages/client/clientAPI/types.ts | 3 +- .../ClientSideBar/ClientSideBar.tsx | 14 +- .../components/ClientSideBar/style.scss | 2 + src/pages/client/hooks/useClientFlags.tsx | 33 +++ .../pages/CarouselPage/CarouselPage.tsx | 52 ++++ .../CarouselPage/cards/CarouselCards.tsx | 233 ++++++++++++++++++ .../CarouselPage/cards/assets/slide_2fa.png | Bin 0 -> 33514 bytes .../cards/assets/slide_instances.png | Bin 0 -> 50806 bytes .../cards/assets/slide_security.png | Bin 0 -> 85292 bytes .../pages/CarouselPage/cards/style.scss | 0 .../components/CardCarousel/CardCarousel.tsx | 65 +++++ .../CarouselControl/CarouselControl.tsx | 34 +++ .../components/CarouselControl/style.scss | 47 ++++ .../components/CardCarousel/style.scss | 30 +++ .../components/CardCarousel/types.ts | 6 + .../client/pages/CarouselPage/style.scss | 205 +++++++++++++++ .../icons/IconDefguard/IconDeguard.tsx | 41 +++ .../IconDefguardFull/IconDefguardFull.tsx | 45 ++++ src/shared/links.ts | 3 + src/shared/routes.ts | 1 + 30 files changed, 1187 insertions(+), 12 deletions(-) create mode 100644 src/pages/client/hooks/useClientFlags.tsx create mode 100644 src/pages/client/pages/CarouselPage/CarouselPage.tsx create mode 100644 src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx create mode 100644 src/pages/client/pages/CarouselPage/cards/assets/slide_2fa.png create mode 100644 src/pages/client/pages/CarouselPage/cards/assets/slide_instances.png create mode 100644 src/pages/client/pages/CarouselPage/cards/assets/slide_security.png create mode 100644 src/pages/client/pages/CarouselPage/cards/style.scss create mode 100644 src/pages/client/pages/CarouselPage/components/CardCarousel/CardCarousel.tsx create mode 100644 src/pages/client/pages/CarouselPage/components/CardCarousel/components/CarouselControl/CarouselControl.tsx create mode 100644 src/pages/client/pages/CarouselPage/components/CardCarousel/components/CarouselControl/style.scss create mode 100644 src/pages/client/pages/CarouselPage/components/CardCarousel/style.scss create mode 100644 src/pages/client/pages/CarouselPage/components/CardCarousel/types.ts create mode 100644 src/pages/client/pages/CarouselPage/style.scss create mode 100644 src/shared/components/icons/IconDefguard/IconDeguard.tsx create mode 100644 src/shared/components/icons/IconDefguardFull/IconDefguardFull.tsx create mode 100644 src/shared/links.ts diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6dbfea46..a23214c3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1184,6 +1184,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "webbrowser", "x25519-dalek", ] @@ -2507,6 +2508,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -4994,7 +5011,7 @@ dependencies = [ "gtk", "image", "instant", - "jni", + "jni 0.20.0", "lazy_static", "libappindicator", "libc", @@ -5988,6 +6005,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b2391658b02c27719fc5a0a73d6e696285138e8b12fba9d4baa70451023c71" +dependencies = [ + "core-foundation", + "home", + "jni 0.21.1", + "log", + "ndk-context", + "objc", + "raw-window-handle", + "url", + "web-sys", +] + [[package]] name = "webkit2gtk" version = "0.18.2" @@ -6180,6 +6214,15 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -6198,6 +6241,21 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -6243,6 +6301,12 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6261,6 +6325,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6279,6 +6349,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6297,6 +6373,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6315,6 +6397,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6327,6 +6415,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6345,6 +6439,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6caad1f6..db32e8a9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,7 +19,6 @@ anyhow = "1.0" base64 = "0.21" clap = { version = "4.4", features = ["derive", "env"] } chrono = { version = "0.4", features = ["serde"] } -dark-light = "1.0" defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", branch = "main" } dirs = "5.0" lazy_static = "1.4" @@ -35,7 +34,10 @@ serde_with = "3.4" sqlx = { version = "0.7", features = ["chrono", "sqlite", "runtime-tokio", "uuid", "macros"] } struct-patch = "0.4" strum = { version = "0.25", features = ["derive"] } -tauri = { version = "1.5", features = [ "fs-all", "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png"] } +dark-light = "1.0" +webbrowser = "0.8" + +tauri = { version = "1.5", features = [ "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png", "fs-all"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } thiserror = "1.0" diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index f280bcea..7b73097b 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -14,15 +14,15 @@ use defguard_client::{ __cmd__active_connection, __cmd__all_connections, __cmd__all_instances, __cmd__all_locations, __cmd__all_tunnels, __cmd__connect, __cmd__delete_instance, __cmd__disconnect, __cmd__get_settings, __cmd__last_connection, __cmd__location_interface_details, - __cmd__location_stats, __cmd__parse_tunnel_config, __cmd__save_device_config, + __cmd__location_stats, __cmd__open_link, __cmd__parse_tunnel_config, __cmd__save_device_config, __cmd__save_tunnel, __cmd__tunnel_details, __cmd__update_instance, __cmd__update_location_routing, __cmd__update_settings, appstate::AppState, commands::{ active_connection, all_connections, all_instances, all_locations, all_tunnels, connect, delete_instance, disconnect, get_settings, last_connection, location_interface_details, - location_stats, parse_tunnel_config, save_device_config, save_tunnel, tunnel_details, - update_instance, update_location_routing, update_settings, + location_stats, open_link, parse_tunnel_config, save_device_config, save_tunnel, + tunnel_details, update_instance, update_location_routing, update_settings, }, database::{self, models::settings::Settings}, tray::{configure_tray_icon, create_tray_menu, handle_tray_event}, @@ -94,6 +94,7 @@ async fn main() { parse_tunnel_config, save_tunnel, all_tunnels, + open_link, tunnel_details, ]) .on_window_event(|event| match event.event() { diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d3f638d6..7dcdaf62 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -668,3 +668,11 @@ pub async fn tunnel_details( Err(Error::NotFound) } } + +#[tauri::command] +pub async fn open_link(link: &str) -> Result<(), Error> { + match webbrowser::open(link) { + Ok(_) => Ok(()), + Err(e) => Err(Error::CommandError(e.to_string())), + } +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index b521d6a6..384a281f 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -42,6 +42,8 @@ pub enum Error { ConfigParseError(String), #[error("Failed to acquire mutex lock")] MutexError, + #[error("Command failed: {0}")] + CommandError(String), } // we must manually implement serde::Serialize diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 1b5d3c2b..c34f1175 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -23,6 +23,7 @@ import { loadLocaleAsync } from '../../i18n/i18n-util.async'; import { clientApi } from '../../pages/client/clientAPI/clientApi'; import { ClientPage } from '../../pages/client/ClientPage'; import { useClientStore } from '../../pages/client/hooks/useClientStore'; +import { CarouselPage } from '../../pages/client/pages/CarouselPage/CarouselPage'; import { ClientAddedPage } from '../../pages/client/pages/ClientAddedPage/ClientAddedPage'; import { ClientAddInstancePage } from '../../pages/client/pages/ClientAddInstancePage/ClientAddInstnacePage'; import { ClientAddTunnelPage } from '../../pages/client/pages/ClientAddTunnelPage/ClientAddTunnelPage'; @@ -70,6 +71,10 @@ const router = createBrowserRouter([ index: true, element: , }, + { + path: '/client/carousel', + element: , + }, { path: '/client/add-instance', element: , diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 31a51aa9..ef3f259d 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -52,6 +52,62 @@ const en = { pages: { client: { pages: { + carouselPage: { + slides: { + shared: { + isMore: 'defguard is all the above and more!', + githubButton: 'Visit defguard on', + }, + welcome: { + // md + title: 'Welcome to **defguard** desktop client!', + instance: { + title: 'Add Instance', + subtitle: + 'Establish a connection to defguard instance effortlessly by configuring it with a single token.', + }, + tunel: { + title: 'Add Tunel', + subtitle: + 'Utilize it as a WireGuard® Desktop Client with ease. Set up your own tunnel or import a configuration file.', + }, + }, + twoFa: { + // md + title: 'WireGuard **2FA with defguard**', + // md + sideText: `Since Wireguard protocol doesn't support 2FA/MFA - most (if not all) currently available Wireguard clients do not support real Multi-Factor Authentication/2FA - and use 2FA just as authorization to the "application" itself (and not Wireguard tunnel). + +If you would like to secure your Wireguard instance try **defguard** VPN & SSO server (which is also free & open source) to get real 2FA using Wireguard PSK keys and peers configuration by defguard gateway!`, + }, + security: { + // md + title: 'Security and Privacy **done right!**', + // md + sideText: `* Privacy requires controlling your data, thus your user data (Identity, SSO) needs to be on-premise (on your servers) +* Securing your data and applications requires authentication and authorization (SSO) with Multi-Factor Authentication, and for highest security - MFA with Hardware Security Modules +* Accessing your data and applications securely and privately requires data encryption (HTTPS) and a secure tunnel between your device and the Internet to encrypt all traffic (VPN). +* To fully trust your SSO, VPN, it needs to be Open Source`, + }, + instances: { + // md + title: '**Multiple** instance & locations', + // md + sideText: `**defguard** (both server nad this client) support multiple instances (installations) and multiple Locations (VPN tunnels). + +If you are an admin/devops - all your customers (instances) and all their tunnels (locations) can be in one place!`, + }, + support: { + // md + title: '**Support us** on Github', + // md + text: `**defguard** is free and truly Open Source and our team has been working on it for several months. Please consider supporting us by: +- staring us on GitHub +- spreading the word about **defguard**! +- join our Matrix server: https://matrix.to/#/#defguard:teonite.com`, + }, + }, + }, settingsPage: { title: 'Settings', tabs: { diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 4620fa4f..2ca19009 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -136,6 +136,96 @@ type RootTranslation = { pages: { client: { pages: { + carouselPage: { + slides: { + shared: { + /** + * d​e​f​g​u​a​r​d​ ​i​s​ ​a​l​l​ ​t​h​e​ ​a​b​o​v​e​ ​a​n​d​ ​m​o​r​e​! + */ + isMore: string + /** + * V​i​s​i​t​ ​d​e​f​g​u​a​r​d​ ​o​n + */ + githubButton: string + } + welcome: { + /** + * W​e​l​c​o​m​e​ ​t​o​ ​*​*​d​e​f​g​u​a​r​d​*​*​ ​d​e​s​k​t​o​p​ ​c​l​i​e​n​t​! + */ + title: string + instance: { + /** + * A​d​d​ ​I​n​s​t​a​n​c​e + */ + title: string + /** + * E​s​t​a​b​l​i​s​h​ ​a​ ​c​o​n​n​e​c​t​i​o​n​ ​t​o​ ​d​e​f​g​u​a​r​d​ ​i​n​s​t​a​n​c​e​ ​e​f​f​o​r​t​l​e​s​s​l​y​ ​b​y​ ​c​o​n​f​i​g​u​r​i​n​g​ ​i​t​ ​w​i​t​h​ ​a​ ​s​i​n​g​l​e​ ​t​o​k​e​n​. + */ + subtitle: string + } + tunel: { + /** + * A​d​d​ ​T​u​n​e​l + */ + title: string + /** + * U​t​i​l​i​z​e​ ​i​t​ ​a​s​ ​a​ ​W​i​r​e​G​u​a​r​d​®​ ​D​e​s​k​t​o​p​ ​C​l​i​e​n​t​ ​w​i​t​h​ ​e​a​s​e​.​ ​S​e​t​ ​u​p​ ​y​o​u​r​ ​o​w​n​ ​t​u​n​n​e​l​ ​o​r​ ​i​m​p​o​r​t​ ​a​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​f​i​l​e​. + */ + subtitle: string + } + } + twoFa: { + /** + * W​i​r​e​G​u​a​r​d​ ​*​*​2​F​A​ ​w​i​t​h​ ​d​e​f​g​u​a​r​d​*​* + */ + title: string + /** + * S​i​n​c​e​ ​W​i​r​e​g​u​a​r​d​ ​p​r​o​t​o​c​o​l​ ​d​o​e​s​n​'​t​ ​s​u​p​p​o​r​t​ ​2​F​A​/​M​F​A​ ​-​ ​m​o​s​t​ ​(​i​f​ ​n​o​t​ ​a​l​l​)​ ​c​u​r​r​e​n​t​l​y​ ​a​v​a​i​l​a​b​l​e​ ​W​i​r​e​g​u​a​r​d​ ​c​l​i​e​n​t​s​ ​d​o​ ​n​o​t​ ​s​u​p​p​o​r​t​ ​r​e​a​l​ ​M​u​l​t​i​-​F​a​c​t​o​r​ ​A​u​t​h​e​n​t​i​c​a​t​i​o​n​/​2​F​A​ ​-​ ​a​n​d​ ​u​s​e​ ​2​F​A​ ​j​u​s​t​ ​a​s​ ​a​u​t​h​o​r​i​z​a​t​i​o​n​ ​t​o​ ​t​h​e​ ​"​a​p​p​l​i​c​a​t​i​o​n​"​ ​i​t​s​e​l​f​ ​(​a​n​d​ ​n​o​t​ ​W​i​r​e​g​u​a​r​d​ ​t​u​n​n​e​l​)​.​ ​ ​ + ​ + ​I​f​ ​y​o​u​ ​w​o​u​l​d​ ​l​i​k​e​ ​t​o​ ​s​e​c​u​r​e​ ​y​o​u​r​ ​W​i​r​e​g​u​a​r​d​ ​i​n​s​t​a​n​c​e​ ​t​r​y​ ​*​*​d​e​f​g​u​a​r​d​*​*​ ​V​P​N​ ​&​ ​S​S​O​ ​s​e​r​v​e​r​ ​(​w​h​i​c​h​ ​i​s​ ​a​l​s​o​ ​f​r​e​e​ ​&​ ​o​p​e​n​ ​s​o​u​r​c​e​)​ ​t​o​ ​g​e​t​ ​r​e​a​l​ ​2​F​A​ ​u​s​i​n​g​ ​W​i​r​e​g​u​a​r​d​ ​P​S​K​ ​k​e​y​s​ ​a​n​d​ ​p​e​e​r​s​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​b​y​ ​d​e​f​g​u​a​r​d​ ​g​a​t​e​w​a​y​! + */ + sideText: string + } + security: { + /** + * S​e​c​u​r​i​t​y​ ​a​n​d​ ​P​r​i​v​a​c​y​ ​*​*​d​o​n​e​ ​r​i​g​h​t​!​*​* + */ + title: string + /** + * *​ ​P​r​i​v​a​c​y​ ​r​e​q​u​i​r​e​s​ ​c​o​n​t​r​o​l​l​i​n​g​ ​y​o​u​r​ ​d​a​t​a​,​ ​t​h​u​s​ ​y​o​u​r​ ​u​s​e​r​ ​d​a​t​a​ ​(​I​d​e​n​t​i​t​y​,​ ​S​S​O​)​ ​n​e​e​d​s​ ​t​o​ ​b​e​ ​o​n​-​p​r​e​m​i​s​e​ ​(​o​n​ ​y​o​u​r​ ​s​e​r​v​e​r​s​)​ + ​*​ ​S​e​c​u​r​i​n​g​ ​y​o​u​r​ ​d​a​t​a​ ​a​n​d​ ​a​p​p​l​i​c​a​t​i​o​n​s​ ​r​e​q​u​i​r​e​s​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​a​n​d​ ​a​u​t​h​o​r​i​z​a​t​i​o​n​ ​(​S​S​O​)​ ​w​i​t​h​ ​M​u​l​t​i​-​F​a​c​t​o​r​ ​A​u​t​h​e​n​t​i​c​a​t​i​o​n​,​ ​a​n​d​ ​f​o​r​ ​h​i​g​h​e​s​t​ ​s​e​c​u​r​i​t​y​ ​-​ ​M​F​A​ ​w​i​t​h​ ​H​a​r​d​w​a​r​e​ ​S​e​c​u​r​i​t​y​ ​M​o​d​u​l​e​s​ + ​*​ ​A​c​c​e​s​s​i​n​g​ ​y​o​u​r​ ​d​a​t​a​ ​a​n​d​ ​a​p​p​l​i​c​a​t​i​o​n​s​ ​s​e​c​u​r​e​l​y​ ​a​n​d​ ​p​r​i​v​a​t​e​l​y​ ​r​e​q​u​i​r​e​s​ ​d​a​t​a​ ​e​n​c​r​y​p​t​i​o​n​ ​(​H​T​T​P​S​)​ ​a​n​d​ ​a​ ​s​e​c​u​r​e​ ​t​u​n​n​e​l​ ​b​e​t​w​e​e​n​ ​y​o​u​r​ ​d​e​v​i​c​e​ ​a​n​d​ ​t​h​e​ ​I​n​t​e​r​n​e​t​ ​t​o​ ​e​n​c​r​y​p​t​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​(​V​P​N​)​.​ + ​*​ ​T​o​ ​f​u​l​l​y​ ​t​r​u​s​t​ ​y​o​u​r​ ​S​S​O​,​ ​V​P​N​,​ ​i​t​ ​n​e​e​d​s​ ​t​o​ ​b​e​ ​O​p​e​n​ ​S​o​u​r​c​e + */ + sideText: string + } + instances: { + /** + * *​*​M​u​l​t​i​p​l​e​*​*​ ​i​n​s​t​a​n​c​e​ ​&​ ​l​o​c​a​t​i​o​n​s + */ + title: string + /** + * *​*​d​e​f​g​u​a​r​d​*​*​ ​(​b​o​t​h​ ​s​e​r​v​e​r​ ​n​a​d​ ​t​h​i​s​ ​c​l​i​e​n​t​)​ ​s​u​p​p​o​r​t​ ​m​u​l​t​i​p​l​e​ ​i​n​s​t​a​n​c​e​s​ ​(​i​n​s​t​a​l​l​a​t​i​o​n​s​)​ ​a​n​d​ ​m​u​l​t​i​p​l​e​ ​L​o​c​a​t​i​o​n​s​ ​(​V​P​N​ ​t​u​n​n​e​l​s​)​.​ ​ ​ + ​ + ​I​f​ ​y​o​u​ ​a​r​e​ ​a​n​ ​a​d​m​i​n​/​d​e​v​o​p​s​ ​-​ ​a​l​l​ ​y​o​u​r​ ​c​u​s​t​o​m​e​r​s​ ​(​i​n​s​t​a​n​c​e​s​)​ ​a​n​d​ ​a​l​l​ ​t​h​e​i​r​ ​t​u​n​n​e​l​s​ ​(​l​o​c​a​t​i​o​n​s​)​ ​c​a​n​ ​b​e​ ​i​n​ ​o​n​e​ ​p​l​a​c​e​! + */ + sideText: string + } + support: { + /** + * *​*​S​u​p​p​o​r​t​ ​u​s​*​*​ ​o​n​ ​G​i​t​h​u​b + */ + title: string + /** + * *​*​d​e​f​g​u​a​r​d​*​*​ ​i​s​ ​f​r​e​e​ ​a​n​d​ ​t​r​u​l​y​ ​O​p​e​n​ ​S​o​u​r​c​e​ ​a​n​d​ ​o​u​r​ ​t​e​a​m​ ​h​a​s​ ​b​e​e​n​ ​w​o​r​k​i​n​g​ ​o​n​ ​i​t​ ​f​o​r​ ​s​e​v​e​r​a​l​ ​m​o​n​t​h​s​.​ ​P​l​e​a​s​e​ ​c​o​n​s​i​d​e​r​ ​s​u​p​p​o​r​t​i​n​g​ ​u​s​ ​b​y​:​ ​ ​ + ​-​ ​s​t​a​r​i​n​g​ ​u​s​ ​o​n​ ​G​i​t​H​u​b​ + ​-​ ​s​p​r​e​a​d​i​n​g​ ​t​h​e​ ​w​o​r​d​ ​a​b​o​u​t​ ​*​*​d​e​f​g​u​a​r​d​*​*​!​ + ​-​ ​j​o​i​n​ ​o​u​r​ ​M​a​t​r​i​x​ ​s​e​r​v​e​r​:​ ​h​t​t​p​s​:​/​/​m​a​t​r​i​x​.​t​o​/​#​/​#​d​e​f​g​u​a​r​d​:​t​e​o​n​i​t​e​.​c​o​m + */ + text: string + } + } + } settingsPage: { /** * S​e​t​t​i​n​g​s @@ -1386,6 +1476,96 @@ export type TranslationFunctions = { pages: { client: { pages: { + carouselPage: { + slides: { + shared: { + /** + * defguard is all the above and more! + */ + isMore: () => LocalizedString + /** + * Visit defguard on + */ + githubButton: () => LocalizedString + } + welcome: { + /** + * Welcome to **defguard** desktop client! + */ + title: () => LocalizedString + instance: { + /** + * Add Instance + */ + title: () => LocalizedString + /** + * Establish a connection to defguard instance effortlessly by configuring it with a single token. + */ + subtitle: () => LocalizedString + } + tunel: { + /** + * Add Tunel + */ + title: () => LocalizedString + /** + * Utilize it as a WireGuard® Desktop Client with ease. Set up your own tunnel or import a configuration file. + */ + subtitle: () => LocalizedString + } + } + twoFa: { + /** + * WireGuard **2FA with defguard** + */ + title: () => LocalizedString + /** + * Since Wireguard protocol doesn't support 2FA/MFA - most (if not all) currently available Wireguard clients do not support real Multi-Factor Authentication/2FA - and use 2FA just as authorization to the "application" itself (and not Wireguard tunnel). + + If you would like to secure your Wireguard instance try **defguard** VPN & SSO server (which is also free & open source) to get real 2FA using Wireguard PSK keys and peers configuration by defguard gateway! + */ + sideText: () => LocalizedString + } + security: { + /** + * Security and Privacy **done right!** + */ + title: () => LocalizedString + /** + * * Privacy requires controlling your data, thus your user data (Identity, SSO) needs to be on-premise (on your servers) + * Securing your data and applications requires authentication and authorization (SSO) with Multi-Factor Authentication, and for highest security - MFA with Hardware Security Modules + * Accessing your data and applications securely and privately requires data encryption (HTTPS) and a secure tunnel between your device and the Internet to encrypt all traffic (VPN). + * To fully trust your SSO, VPN, it needs to be Open Source + */ + sideText: () => LocalizedString + } + instances: { + /** + * **Multiple** instance & locations + */ + title: () => LocalizedString + /** + * **defguard** (both server nad this client) support multiple instances (installations) and multiple Locations (VPN tunnels). + + If you are an admin/devops - all your customers (instances) and all their tunnels (locations) can be in one place! + */ + sideText: () => LocalizedString + } + support: { + /** + * **Support us** on Github + */ + title: () => LocalizedString + /** + * **defguard** is free and truly Open Source and our team has been working on it for several months. Please consider supporting us by: + - staring us on GitHub + - spreading the word about **defguard**! + - join our Matrix server: https://matrix.to/#/#defguard:teonite.com + */ + text: () => LocalizedString + } + } + } settingsPage: { /** * Settings diff --git a/src/pages/client/ClientPage.tsx b/src/pages/client/ClientPage.tsx index 5b16898b..a7ee7781 100644 --- a/src/pages/client/ClientPage.tsx +++ b/src/pages/client/ClientPage.tsx @@ -3,10 +3,12 @@ import './style.scss'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { listen, UnlistenFn } from '@tauri-apps/api/event'; import { useEffect } from 'react'; -import { Outlet } from 'react-router-dom'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { routes } from '../../shared/routes'; import { clientApi } from './clientAPI/clientApi'; import { ClientSideBar } from './components/ClientSideBar/ClientSideBar'; +import { useClientFlags } from './hooks/useClientFlags'; import { useClientStore } from './hooks/useClientStore'; import { clientQueryKeys } from './query'; import { TauriEventKey } from './types'; @@ -19,6 +21,9 @@ export const ClientPage = () => { state.setInstances, state.setTunnels, ]); + const navigate = useNavigate(); + const firstLaunch = useClientFlags((state) => state.firstStart); + const location = useLocation(); const { data: instances } = useQuery({ queryFn: getInstances, @@ -96,6 +101,13 @@ export const ClientPage = () => { } }, [instances, setInstances, tunnels, setTunnels]); + // navigate to carousel on first app Launch + useEffect(() => { + if (!location.pathname.includes(routes.client.carousel) && firstLaunch) { + navigate(routes.client.carousel, { replace: true }); + } + }, [firstLaunch, navigate, location.pathname]); + return ( <> diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index 8cebd845..587bcab6 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -100,6 +100,10 @@ const getLocationDetails = async ( const getTunnels = async (): Promise => invokeWrapper('all_tunnels'); +// opens given link in system default browser +const openLink = async (link: string): Promise => + invokeWrapper('open_link', { link }); + const getTunnelDetails = async (id: number): Promise => invokeWrapper('tunnel_details', { tunnelId: id }); @@ -122,5 +126,6 @@ export const clientApi = { updateInstance, parseTunnelConfig, saveTunnel, + openLink, getTunnelDetails, }; diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 0d68075c..9a27fae3 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -123,4 +123,5 @@ export type TauriCommandKey = | 'save_tunnel' | 'all_tunnels' | 'tunnel_details' - | 'location_interface_details'; + | 'location_interface_details' + | 'open_link'; diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index cc13ceae..f79675bc 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -4,8 +4,8 @@ import classNames from 'classnames'; import { useMatch, useNavigate } from 'react-router-dom'; import { useI18nContext } from '../../../../i18n/i18n-react'; +import { IconDefguard } from '../../../../shared/components/icons/IconDefguard/IconDeguard'; import SvgDefguadNavLogoCollapsed from '../../../../shared/components/svg/DefguardLogoCollapsed'; -import SvgDefguardLogoIcon from '../../../../shared/components/svg/DefguardLogoIcon'; import SvgDefguardLogoText from '../../../../shared/components/svg/DefguardLogoText'; import SvgIconNavConnections from '../../../../shared/components/svg/IconNavConnections'; import SvgIconNavVpn from '../../../../shared/components/svg/IconNavVpn'; @@ -30,11 +30,17 @@ export const ClientSideBar = () => { return (
-
- +
navigate(routes.client.carousel, { replace: true })} + > +
-
+
navigate(routes.client.carousel, { replace: true })} + >
diff --git a/src/pages/client/components/ClientSideBar/style.scss b/src/pages/client/components/ClientSideBar/style.scss index 14df94f5..62ade847 100644 --- a/src/pages/client/components/ClientSideBar/style.scss +++ b/src/pages/client/components/ClientSideBar/style.scss @@ -27,6 +27,7 @@ height: 108px; column-gap: 7px; border-bottom: 1px solid var(--border-primary); + cursor: pointer; @include media-breakpoint-up(lg) { display: flex; @@ -48,6 +49,7 @@ align-items: center; justify-content: center; box-sizing: border-box; + cursor: pointer; @include media-breakpoint-up(lg) { display: none; diff --git a/src/pages/client/hooks/useClientFlags.tsx b/src/pages/client/hooks/useClientFlags.tsx new file mode 100644 index 00000000..0b92a196 --- /dev/null +++ b/src/pages/client/hooks/useClientFlags.tsx @@ -0,0 +1,33 @@ +import { createJSONStorage, persist } from 'zustand/middleware'; +import { createWithEqualityFn } from 'zustand/traditional'; + +const defaults: StoreValues = { + firstStart: true, +}; + +/*Flags that are persisted via localstorage and are not used by rust backend*/ +export const useClientFlags = createWithEqualityFn()( + persist( + (set) => ({ + ...defaults, + setValues: (vals) => set({ ...vals }), + }), + { + name: 'client-flags', + version: 1, + storage: createJSONStorage(() => localStorage), + }, + ), + Object.is, +); + +type Store = StoreValues & StoreMethods; + +type StoreValues = { + // Is user launching app first time ? + firstStart: boolean; +}; + +type StoreMethods = { + setValues: (values: Partial) => void; +}; diff --git a/src/pages/client/pages/CarouselPage/CarouselPage.tsx b/src/pages/client/pages/CarouselPage/CarouselPage.tsx new file mode 100644 index 00000000..c749905b --- /dev/null +++ b/src/pages/client/pages/CarouselPage/CarouselPage.tsx @@ -0,0 +1,52 @@ +import './style.scss'; + +import { useEffect } from 'react'; + +import { useClientFlags } from '../../hooks/useClientFlags'; +import { + InstancesSlide, + SecuritySlide, + SupportSlide, + TwoFaSlide, + WelcomeCardSlide, +} from './cards/CarouselCards'; +import { CardCarousel } from './components/CardCarousel/CardCarousel'; +import { CarouselItem } from './components/CardCarousel/types'; + +const slides: CarouselItem[] = [ + { + key: 'welcome', + element: , + }, + { + key: 'twofa', + element: , + }, + { + element: , + key: 'security', + }, + { + key: 'instances', + element: , + }, + { + key: 'support', + element: , + }, +]; + +export const CarouselPage = () => { + const setClientFlags = useClientFlags((state) => state.setValues); + + useEffect(() => { + setClientFlags({ firstStart: false }); + // eslint-next-line-ignore + }, [setClientFlags]); + + return ( + + ); +}; diff --git a/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx new file mode 100644 index 00000000..9478655c --- /dev/null +++ b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx @@ -0,0 +1,233 @@ +import './style.scss'; + +import Markdown from 'react-markdown'; +import { useNavigate } from 'react-router-dom'; + +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { IconDefguard } from '../../../../../shared/components/icons/IconDefguard/IconDeguard'; +import SvgDefguardLogoText from '../../../../../shared/components/svg/DefguardLogoText'; +import { Button } from '../../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../../shared/defguard-ui/components/Layout/Button/types'; +import { Card } from '../../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { defguardGithubLink } from '../../../../../shared/links'; +import { routes } from '../../../../../shared/routes'; +import { clientApi } from '../../../clientAPI/clientApi'; +import twoFactorImage from './assets/slide_2fa.png'; +import instancesImage from './assets/slide_instances.png'; +import securityImage from './assets/slide_security.png'; + +const { openLink } = clientApi; + +export const WelcomeCardSlide = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.carouselPage.slides.welcome; + const navigate = useNavigate(); + + return ( + +

+ {localLL.title()} +

+
+
navigate(routes.client.addInstance, { replace: true })} + > +

{localLL.instance.title()}

+

{localLL.instance.subtitle()}

+
+ + +
+
+
navigate(routes.client.addTunnel, { replace: true })} + > +

{localLL.tunel.title()}

+

{localLL.tunel.subtitle()}

+ +
+
+
+ ); +}; + +export const TwoFaSlide = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.carouselPage.slides.twoFa; + return ( + +

+ {localLL.title()} +

+
+ +
+ {localLL.sideText()} +
+
+ +
+ ); +}; + +const GithubIcon = () => { + return ( + + + + + + + ); +}; + +const GithubButton = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.carouselPage.slides.shared; + return ( + + ))} +
+ ); +}; diff --git a/src/pages/client/pages/CarouselPage/components/CardCarousel/components/CarouselControl/style.scss b/src/pages/client/pages/CarouselPage/components/CardCarousel/components/CarouselControl/style.scss new file mode 100644 index 00000000..7feb1f31 --- /dev/null +++ b/src/pages/client/pages/CarouselPage/components/CardCarousel/components/CarouselControl/style.scss @@ -0,0 +1,47 @@ +@use '@scssutils' as *; + +.carousel-control { + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + width: 100%; + height: 40px; + gap: 0; + + & > button { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + width: 40px; + height: 100%; + position: relative; + overflow: hidden; + cursor: pointer; + border: 0px solid transparent; + background: none; + + .dot { + display: block; + content: ' '; + height: 14px; + width: 14px; + background-color: var(--surface-scroll-inactive); + transition-property: background-color; + transition-timing-function: ease-in-out; + transition-duration: 50ms; + border-radius: 50%; + + &.active { + background-color: var(--surface-main-primary); + } + } + + &:hover { + .dot { + background-color: var(--surface-main-primary); + } + } + } +} diff --git a/src/pages/client/pages/CarouselPage/components/CardCarousel/style.scss b/src/pages/client/pages/CarouselPage/components/CardCarousel/style.scss new file mode 100644 index 00000000..631017c3 --- /dev/null +++ b/src/pages/client/pages/CarouselPage/components/CardCarousel/style.scss @@ -0,0 +1,30 @@ +@use '@scssutils' as *; + +.card-carousel { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + row-gap: 30px; + min-width: 900px; + overflow-x: auto; + + & > .card-wrapper { + display: block; + width: 100%; + max-width: 1200px; + + & > .card { + width: 100%; + max-width: inherit; + min-height: 720px; + overflow: hidden; + box-sizing: border-box; + padding: 60px; + display: flex; + flex-flow: column; + align-items: center; + justify-content: flex-start; + } + } +} diff --git a/src/pages/client/pages/CarouselPage/components/CardCarousel/types.ts b/src/pages/client/pages/CarouselPage/components/CardCarousel/types.ts new file mode 100644 index 00000000..c836137a --- /dev/null +++ b/src/pages/client/pages/CarouselPage/components/CardCarousel/types.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; + +export type CarouselItem = { + key: string; + element: ReactNode; +}; diff --git a/src/pages/client/pages/CarouselPage/style.scss b/src/pages/client/pages/CarouselPage/style.scss new file mode 100644 index 00000000..fec9378b --- /dev/null +++ b/src/pages/client/pages/CarouselPage/style.scss @@ -0,0 +1,205 @@ +@use '@scssutils' as *; + +#carousel-page { + .card { + box-sizing: border-box; + padding: 60px; + + .github { + height: 60px; + width: 270px; + + p, + span { + @include typography(app-button-l); + text-decoration: none; + } + } + + .more { + @include typography(app-body-1); + } + + ul { + margin: 0; + } + + h2 { + font-family: 'Poppins'; + font-size: 48px; + font-style: normal; + color: var(--text-body-primary); + line-height: normal; + text-align: center; + width: 100%; + font-weight: 400; + height: 72px; + } + + strong, + b { + font-weight: 700; + } + + .row { + width: 100%; + display: grid; + grid-template-rows: auto auto; + grid-template-columns: 1fr; + row-gap: 20px; + + @include media-breakpoint-up(xl) { + grid-template-rows: auto; + grid-template-columns: 1fr 1fr; + column-gap: 40px; + row-gap: 0; + } + + & > .image-box { + width: 100%; + height: 301px; + border: none; + border-radius: 15px; + box-shadow: var(--box-shadow); + background-size: cover; + } + + &.between { + @include media-breakpoint-up(xl) { + grid-template-columns: auto auto; + grid-template-rows: 1fr; + justify-items: space-between; + } + } + + .text { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-content: flex-start; + row-gap: 20px; + max-width: 650px; + @include typography(app-welcome-2); + text-align: center; + + @include media-breakpoint-up(xl) { + text-align: left; + } + + strong, + b { + font-weight: 700; + } + + &.centered { + justify-content: center; + } + } + } + } + + #welcome-slide { + h2 { + margin-bottom: 40px; + } + + & > .row { + padding: 0 60px; + } + + .wireguard-logo { + path { + fill: var(--text-body-primary); + } + } + + .logo-container { + width: 100%; + height: 85px; + display: flex; + align-items: center; + justify-content: center; + flex-flow: row nowrap; + column-gap: 13px; + + :nth-child(1) { + width: 40px; + height: 100%; + } + + :nth-child(2) { + width: 159px; + height: 46px; + + path { + fill: var(--text-body-primary); + } + } + } + + .inner-card { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + background-color: var(--surface-frame-bg); + border-radius: 15px; + box-shadow: var(--box-shadow); + min-height: 415px; + box-sizing: border-box; + padding: 64px; + width: 420px; + overflow: hidden; + user-select: none; + + h3 { + @include typography(app-body-1); + margin-bottom: 20px; + } + + p { + @include typography(welcome-h2); + text-align: center; + color: var(--text-body-tertiary); + margin-bottom: 45px; + max-width: 100%; + } + } + } + + #factor-slide, + #security-slide, + #instances-slide, + #support-slide { + justify-content: space-between; + } + + #support-slide { + .text { + max-width: 600px; + user-select: text; + + p { + margin-bottom: 20px; + } + } + + .logo-container { + height: 118px; + width: 100%; + display: flex; + flex-flow: row nowrap; + column-gap: 18px; + + :nth-child(1) { + height: 100%; + } + + :nth-child(2) { + path { + fill: var(--text-body-primary); + } + } + } + } +} diff --git a/src/shared/components/icons/IconDefguard/IconDeguard.tsx b/src/shared/components/icons/IconDefguard/IconDeguard.tsx new file mode 100644 index 00000000..e8d8d9f9 --- /dev/null +++ b/src/shared/components/icons/IconDefguard/IconDeguard.tsx @@ -0,0 +1,41 @@ +import { useId } from 'react'; + +type Props = { + height?: number; + width?: number; + id?: string; + className?: string; +}; + +export const IconDefguard = ({ id, className, height = 44, width = 21 }: Props) => { + const gradientId = useId(); + return ( + + + + + + + + + + ); +}; diff --git a/src/shared/components/icons/IconDefguardFull/IconDefguardFull.tsx b/src/shared/components/icons/IconDefguardFull/IconDefguardFull.tsx new file mode 100644 index 00000000..e1403e0f --- /dev/null +++ b/src/shared/components/icons/IconDefguardFull/IconDefguardFull.tsx @@ -0,0 +1,45 @@ +import { useId } from 'react'; + +type Props = { + height?: number; + width?: number; + id?: string; + className?: string; +}; + +export const IconDefguardFull = ({ id, className, height = 56, width = 128 }: Props) => { + const gradientId = useId(); + return ( + + + + + + + + + + + ); +}; diff --git a/src/shared/links.ts b/src/shared/links.ts new file mode 100644 index 00000000..ca3b4e4b --- /dev/null +++ b/src/shared/links.ts @@ -0,0 +1,3 @@ +// collection of external links + +export const defguardGithubLink = 'https://github.com/DefGuard'; diff --git a/src/shared/routes.ts b/src/shared/routes.ts index a3a4ef36..23f553eb 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -10,6 +10,7 @@ export const routes = { instanceCreated: '/client/instance-created', editTunnel: '/client/edit-tunnel', settings: '/client/settings', + carousel: '/client/carousel', }, enrollment: '/enrollment', timeout: '/timeout', From 8178ca6843e19df361d5474967d84414fea8e07f Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Thu, 4 Jan 2024 13:29:57 +0100 Subject: [PATCH 23/45] feat: Delete tunnel (#148) --- ...8b6c65dc2849f3ceaf95d369f95731026b2cb.json | 12 +++ src-tauri/src/bin/defguard-client.rs | 19 +++-- src-tauri/src/commands.rs | 31 +++++++ src-tauri/src/database/models/tunnel.rs | 17 ++++ src/i18n/en/index.ts | 11 +++ src/i18n/i18n-types.ts | 53 ++++++++++++ src/pages/client/clientAPI/clientApi.ts | 4 + src/pages/client/clientAPI/types.ts | 1 + .../ClientEditTunnelPage.tsx | 66 +++++++++------ .../DeleteTunnelModal/DeleteTunnelModal.tsx | 82 +++++++++++++++++++ .../DeleteTunnelModal/useDeleteTunnelModal.ts | 31 +++++++ 11 files changed, 295 insertions(+), 32 deletions(-) create mode 100644 src-tauri/.sqlx/query-c6a8aa6bb12689e5ce241f330268b6c65dc2849f3ceaf95d369f95731026b2cb.json create mode 100644 src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/DeleteTunnelModal.tsx create mode 100644 src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/useDeleteTunnelModal.ts diff --git a/src-tauri/.sqlx/query-c6a8aa6bb12689e5ce241f330268b6c65dc2849f3ceaf95d369f95731026b2cb.json b/src-tauri/.sqlx/query-c6a8aa6bb12689e5ce241f330268b6c65dc2849f3ceaf95d369f95731026b2cb.json new file mode 100644 index 00000000..952d7f88 --- /dev/null +++ b/src-tauri/.sqlx/query-c6a8aa6bb12689e5ce241f330268b6c65dc2849f3ceaf95d369f95731026b2cb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM tunnel WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "c6a8aa6bb12689e5ce241f330268b6c65dc2849f3ceaf95d369f95731026b2cb" +} diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 7b73097b..60b8cc67 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -12,17 +12,19 @@ use tauri_plugin_log::LogTarget; use defguard_client::{ __cmd__active_connection, __cmd__all_connections, __cmd__all_instances, __cmd__all_locations, - __cmd__all_tunnels, __cmd__connect, __cmd__delete_instance, __cmd__disconnect, - __cmd__get_settings, __cmd__last_connection, __cmd__location_interface_details, - __cmd__location_stats, __cmd__open_link, __cmd__parse_tunnel_config, __cmd__save_device_config, - __cmd__save_tunnel, __cmd__tunnel_details, __cmd__update_instance, - __cmd__update_location_routing, __cmd__update_settings, + __cmd__all_tunnels, __cmd__connect, __cmd__delete_instance, __cmd__delete_tunnel, + __cmd__disconnect, __cmd__get_settings, __cmd__last_connection, + __cmd__location_interface_details, __cmd__location_stats, __cmd__open_link, + __cmd__parse_tunnel_config, __cmd__save_device_config, __cmd__save_tunnel, + __cmd__tunnel_details, __cmd__update_instance, __cmd__update_location_routing, + __cmd__update_settings, appstate::AppState, commands::{ active_connection, all_connections, all_instances, all_locations, all_tunnels, connect, - delete_instance, disconnect, get_settings, last_connection, location_interface_details, - location_stats, open_link, parse_tunnel_config, save_device_config, save_tunnel, - tunnel_details, update_instance, update_location_routing, update_settings, + delete_instance, delete_tunnel, disconnect, get_settings, last_connection, + location_interface_details, location_stats, open_link, parse_tunnel_config, + save_device_config, save_tunnel, tunnel_details, update_instance, update_location_routing, + update_settings, }, database::{self, models::settings::Settings}, tray::{configure_tray_icon, create_tray_menu, handle_tray_event}, @@ -96,6 +98,7 @@ async fn main() { all_tunnels, open_link, tunnel_details, + delete_tunnel, ]) .on_window_event(|event| match event.event() { tauri::WindowEvent::CloseRequested { api, .. } => { diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 7dcdaf62..88ba7d36 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -669,6 +669,37 @@ pub async fn tunnel_details( } } +#[tauri::command(async)] +pub async fn delete_tunnel(tunnel_id: i64, handle: AppHandle) -> Result<(), Error> { + debug!("Deleting tunnel {tunnel_id}"); + let app_state = handle.state::(); + let mut client = app_state.client.clone(); + let pool = &app_state.get_pool(); + if let Some(tunnel) = Tunnel::find_by_id(pool, tunnel_id).await? { + if let Some(connection) = + app_state.find_and_remove_connection(tunnel_id, &ConnectionType::Tunnel) + { + debug!("Found active connection for tunnel({tunnel_id}), closing...",); + let request = RemoveInterfaceRequest { + interface_name: connection.interface_name.clone(), + pre_down: tunnel.pre_down.clone(), + post_down: tunnel.post_up.clone(), + }; + client + .remove_interface(request) + .await + .map_err(|_| Error::InternalError)?; + debug!("Connection closed and interface removed"); + } + tunnel.delete(pool).await?; + } else { + error!("Tunnel {tunnel_id} not found"); + return Err(Error::NotFound); + } + info!("Tunnel {tunnel_id}, deleted"); + Ok(()) +} + #[tauri::command] pub async fn open_link(link: &str) -> Result<(), Error> { match webbrowser::open(link) { diff --git a/src-tauri/src/database/models/tunnel.rs b/src-tauri/src/database/models/tunnel.rs index fa8808cc..450c57fe 100644 --- a/src-tauri/src/database/models/tunnel.rs +++ b/src-tauri/src/database/models/tunnel.rs @@ -161,6 +161,23 @@ impl Tunnel { .fetch_one(pool) .await } + pub async fn delete_by_id(pool: &DbPool, id: i64) -> Result<(), Error> { + // delete instance + query!("DELETE FROM tunnel WHERE id = $1", id) + .execute(pool) + .await?; + Ok(()) + } + + pub async fn delete(&self, pool: &DbPool) -> Result<(), Error> { + match self.id { + Some(id) => { + Tunnel::delete_by_id(pool, id).await?; + Ok(()) + } + None => Err(Error::NotFound), + } + } } #[derive(FromRow, Debug, Serialize, Deserialize)] diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index ef3f259d..a9c3bfc2 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -638,6 +638,17 @@ If you want to disengage your VPN connection, simply press "deactivate". submit: 'Delete instance', }, }, + deleteTunnel: { + title: 'Delete tunnel', + subtitle: 'Are you sure you want to delete {name: string}?', + messages: { + success: 'Tunnel deleted', + error: 'Unexpected error occured', + }, + controls: { + submit: 'Delete tunnel', + }, + }, }, } satisfies BaseTranslation; diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 2ca19009..972909f4 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -1351,6 +1351,33 @@ type RootTranslation = { submit: string } } + deleteTunnel: { + /** + * D​e​l​e​t​e​ ​t​u​n​n​e​l + */ + title: string + /** + * A​r​e​ ​y​o​u​ ​s​u​r​e​ ​y​o​u​ ​w​a​n​t​ ​t​o​ ​d​e​l​e​t​e​ ​{​n​a​m​e​}​? + * @param {string} name + */ + subtitle: RequiredParams<'name'> + messages: { + /** + * T​u​n​n​e​l​ ​d​e​l​e​t​e​d + */ + success: string + /** + * U​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​e​d + */ + error: string + } + controls: { + /** + * D​e​l​e​t​e​ ​t​u​n​n​e​l + */ + submit: string + } + } } } @@ -2684,6 +2711,32 @@ export type TranslationFunctions = { submit: () => LocalizedString } } + deleteTunnel: { + /** + * Delete tunnel + */ + title: () => LocalizedString + /** + * Are you sure you want to delete {name}? + */ + subtitle: (arg: { name: string }) => LocalizedString + messages: { + /** + * Tunnel deleted + */ + success: () => LocalizedString + /** + * Unexpected error occured + */ + error: () => LocalizedString + } + controls: { + /** + * Delete tunnel + */ + submit: () => LocalizedString + } + } } } diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index 587bcab6..243aa5c8 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -107,6 +107,9 @@ const openLink = async (link: string): Promise => const getTunnelDetails = async (id: number): Promise => invokeWrapper('tunnel_details', { tunnelId: id }); +const deleteTunnel = async (id: number): Promise => + invokeWrapper('delete_tunnel', { tunnelId: id }); + export const clientApi = { getInstances, getTunnels, @@ -122,6 +125,7 @@ export const clientApi = { getSettings, updateSettings, deleteInstance, + deleteTunnel, getLocationDetails, updateInstance, parseTunnelConfig, diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 9a27fae3..3712d07b 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -123,5 +123,6 @@ export type TauriCommandKey = | 'save_tunnel' | 'all_tunnels' | 'tunnel_details' + | 'delete_tunnel' | 'location_interface_details' | 'open_link'; diff --git a/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx b/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx index 7b40f04d..d91e8ad0 100644 --- a/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx +++ b/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx @@ -17,6 +17,8 @@ import { useClientStore } from '../../hooks/useClientStore'; import { clientQueryKeys } from '../../query'; import { WireguardInstanceType } from '../../types'; import { EditTunnelFormCard } from './components/EditTunnelFormCard'; +import { DeleteTunnelModal } from './modals/DeleteTunnelModal/DeleteTunnelModal'; +import { useDeleteTunnelModal } from './modals/DeleteTunnelModal/useDeleteTunnelModal'; const { getTunnelDetails } = clientApi; @@ -25,6 +27,7 @@ export const ClientEditTunnelPage = () => { const navigate = useNavigate(); const submitRef = useRef(null); const selectedInstance = useClientStore((state) => state.selectedInstance); + const openDeleteTunnel = useDeleteTunnelModal((state) => state.open); useEffect(() => { if ( selectedInstance?.id === undefined || @@ -40,30 +43,45 @@ export const ClientEditTunnelPage = () => { enabled: !!selectedInstance?.id, }); return ( -
-
-

{LL.pages.client.pages.editTunnelPage.title()}

-
-
+
+
+ {tunnel && }
- -
- {tunnel && } -
-
+
+ + ); }; diff --git a/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/DeleteTunnelModal.tsx b/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/DeleteTunnelModal.tsx new file mode 100644 index 00000000..dda20902 --- /dev/null +++ b/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/DeleteTunnelModal.tsx @@ -0,0 +1,82 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { isUndefined } from 'lodash-es'; +import { useNavigate } from 'react-router-dom'; +import { shallow } from 'zustand/shallow'; + +import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { ConfirmModal } from '../../../../../../shared/defguard-ui/components/Layout/modals/ConfirmModal/ConfirmModal'; +import { ConfirmModalType } from '../../../../../../shared/defguard-ui/components/Layout/modals/ConfirmModal/types'; +import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; +import { routes } from '../../../../../../shared/routes'; +import { clientApi } from '../../../../clientAPI/clientApi'; +import { useClientStore } from '../../../../hooks/useClientStore'; +import { clientQueryKeys } from '../../../../query'; +import { WireguardInstanceType } from '../../../../types'; +import { useDeleteTunnelModal } from './useDeleteTunnelModal'; + +const { deleteTunnel } = clientApi; + +const invalidateOnSuccess = [clientQueryKeys.getTunnels, clientQueryKeys.getConnections]; + +export const DeleteTunnelModal = () => { + const { LL } = useI18nContext(); + const navigate = useNavigate(); + const setClientStore = useClientStore((state) => state.setState); + const [isOpen, tunnel] = useDeleteTunnelModal( + (state) => [state.isOpen, state.tunnel], + shallow, + ); + const [close, reset] = useDeleteTunnelModal( + (state) => [state.close, state.reset], + shallow, + ); + const toaster = useToaster(); + const localLL = LL.modals.deleteTunnel; + const queryClient = useQueryClient(); + + const { mutate, isPending } = useMutation({ + mutationFn: deleteTunnel, + onSuccess: () => { + toaster.success(localLL.messages.success()); + invalidateOnSuccess.forEach((key) => { + queryClient.invalidateQueries({ + queryKey: [key], + refetchType: 'active', + }); + }); + reset(); + setClientStore({ + selectedInstance: { + id: undefined, + type: WireguardInstanceType.TUNNEL, + }, + }); + navigate(routes.client.base, { replace: true }); + }, + onError: (e) => { + toaster.error(localLL.messages.error()); + console.error(e); + }, + }); + + return ( + close()} + afterClose={() => reset()} + loading={isPending} + submitText={localLL.controls.submit()} + cancelText={LL.common.controls.cancel()} + onSubmit={() => { + if (tunnel) { + mutate(tunnel.id); + } + }} + onCancel={() => close()} + /> + ); +}; diff --git a/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/useDeleteTunnelModal.ts b/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/useDeleteTunnelModal.ts new file mode 100644 index 00000000..213b89d6 --- /dev/null +++ b/src/pages/client/pages/ClientEditTunnelPage/modals/DeleteTunnelModal/useDeleteTunnelModal.ts @@ -0,0 +1,31 @@ +import { createWithEqualityFn } from 'zustand/traditional'; + +import { Tunnel } from '../../../../types'; + +const defaultValues: StoreValues = { + isOpen: false, + tunnel: undefined, +}; + +export const useDeleteTunnelModal = createWithEqualityFn( + (set) => ({ + ...defaultValues, + open: (tunnel) => set({ tunnel, isOpen: true }), + close: () => set({ isOpen: false }), + reset: () => set(defaultValues), + }), + Object.is, +); + +type Store = StoreValues & StoreMethods; + +type StoreValues = { + isOpen: boolean; + tunnel?: Tunnel; +}; + +type StoreMethods = { + open: (tunnel: Tunnel) => void; + close: () => void; + reset: () => void; +}; From 614efcbd4791616dbb952ae9db57b9b4f6599768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Thu, 4 Jan 2024 21:49:11 +0100 Subject: [PATCH 24/45] fix: macos tauri resources conf (#149) --- .../resources/icons/tray-32x32-black.png | Bin 2088 -> 514 bytes .../resources/icons/tray-32x32-color.png | Bin 1704 -> 1573 bytes src-tauri/resources/icons/tray-32x32-gray.png | Bin 2090 -> 526 bytes .../resources/icons/tray-32x32-white.png | Bin 2078 -> 501 bytes src-tauri/tauri.macos.conf.json | 3 ++- 5 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/resources/icons/tray-32x32-black.png b/src-tauri/resources/icons/tray-32x32-black.png index e0f7e171e9ce1775c6dd57cf250aa0c49a65b0df..d543b980776037ad757cbc30710e83e6c0a7c16d 100644 GIT binary patch delta 480 zcmV<60U!RT5P}4d83+Ub007x@vVV~w7k>e?Nkl~VMkM7{Qf?!Rl}iUHk`aYM85uEQYJfzNP$&jSQYV*kiIRc4Ohhinwp;B9$Kd;p z(mQ$gT3=7Qwcl@T3{*rp9XP-3C<5y6GR%KX0Cwz_tHofSC=CEMvYBLpOak-)5PwTI zmtxQ9Boemh*TUjbEpnT zzCrQ}fD4!9O1R@d3qTl+9CJiHp%SYi25_nZYRF=S zHz~a^O)fUcN1!TTRZ{3>-PF73u7BtuPEM!+L^pI1Ni@CO%ku`M*mM9_v=B}NZQNOv z9rYP_xWV9!MV9c&o$%;=MmNQ*SVMeenF1QLbAXd})1T5xgr7;|4{4TMiFf)J(BiY`?Jky3jB zk|Fv4V%CuuM-U{E3RearWip#n5Q?DmIAO#o8lwn?AQ_rQLO&GB`FJN&XnQRr2CuBB zOHm{S$Nhf4∾rz6zW)o6R^u;S_~I1SZ#d6*ho*<)m>28<07lASr_AMN~%CAyz9^ z6j~jN!6Sufy>ci|Fk^Utm2gr=s4)eBJQv1E)joGnoab=h1|Hy5WQZlhSjitQU_bEFmkIH86}&LLQ5ji)tl+7Xn$V_HiJy2HH$gqmh^lA7B;H zS0sw=@k$kpTSh2@E)`k0TJZ9sUrv5#1F*3Qu%fVP6h@L5VJ;$RhB7e*)2py{1ThA+ zi@e~h4MX)93DNo@l4A4}LmM7}!k*z-g?%QN=NPBx^RO^r!NXPnT=G_+NH~p5hUgZ3 zP#C(?2VL0hOtx26ST6^%ZB`VsFLgiwt-)bX#~#KAuZCwBPRnkn}0VA5)h_r{ZUQU}*)+-7}jD zf0B8bt$Dij=PTuyr?B-WHRJ!W9!d-6V!agrPEH(EgTd7iGgcJ*zx4_}duSYu!{Llm zm*MFOFT(Q~cpacg51?D+Vjf|LvM=wr>f%_)TLQd6wHap1b!7KQ!gVgkCA;!Ml-dkq3wAiKCme zhk9qP>&}WOnX4%ycg5b1-AG`4!s!hbJhD9PYEN5B=S_SuU0_(UG2%k;nW{U@(MJyF zBz+pU#9h0*-IJ8oSkkXO75hhT?uk?Rc`NeXu;!O+OxzY#9<}qvmZ^~I&m$-UBH}GRs#9rTb!&T>Whf~fkz1WzvVS8zdC9f_w>dWG8 zd-TPA4a`9NCogqOv#wjlVcOY4U;O>TL89Yw*GX;Twtds~ewb9Me#^5n^KI?R%HR11 D1X0YD diff --git a/src-tauri/resources/icons/tray-32x32-color.png b/src-tauri/resources/icons/tray-32x32-color.png index 8cc5ae211119f01d7538b7b900ec13a97ff6a547..b5d5d72eaf6d65ead6d2e982b32ebc31e221a4ca 100644 GIT binary patch delta 1557 zcmV+w2I~2!4W$f_BYy?#Nkl@>mcTmPe6jul#G~zrK1JJ8^V3w z)4g{ekxtq8;hjBm&pqF{=ls5N&pG#7PTAlGusmBx5ithChJWI{VrXO-lOu$VE*@t87@GRaA&HS|23)sN+tzcO_n<0J4`21_0g_VhQQyu38aW*4Yz( zkvOddh%M{ti!JZ9P(Gf38G7vAZ$WRbTWvhi+?H?!AaG{3St$VK+&6XlwCLi-+ol?T zxN1V@0K#)iN+g5HsESKTj@<*5Mx1`eDYA)t<(1P@|PoeqZ-ll+_*VTvWZox>PYw79pU!_j>27oI^y!k_@a zPB!zb?E+w))Vu%e^|_&2s_R3yR@ZCOv&?)p$ShxE&(CvM{22==Q6(CWVRv9+1Bs*xd~H9R{MuGu{*63ov00v!k;6Hx7E9T-HI}n`c7L*N z6=X2b37nWEu<||tz|qD>3<6nT)`;v@yX92=rk+g`hY?D#e7Ae2_X80VZK6wv?NxjP z1Cx+lIt&Ctgh#lwsma|lBFB(lT>s*-?jN7TjgXyFl1&oFuEH$=Mq~)C6F= zgGPW?a0D*!xc%-O;3W=3twx%%GFKQrb*}Bq7PIrl>5~I9TpT%Ljx;IjBNQ4JO`_o7 zoFK9!R5o~KKuDS}S^Yu94NYDPXtw^&NHzcP89po_2}O?>seZSlMw8b}>)QaWyaR;q z;f-S^(nqXhpY3obYJee{OW%EhpSJTD=Mx<4di39a&;J6-&g{{NFb>>)00000NkvXX Hu0mjfrIGOz delta 1689 zcmV;K24?xC45$r|BYy^ONklV;)l4$7RjIPPS9qi%dgIzR|L`@Zi0Ajd&o5CBpXPf1MgAuozOPe|=% zvtZH&XcJQ=dYS`$2S8V$MKD5Gk!9_u)bTL~s;)+TR&mxp{-u#vcDvbQ6dc*IVh2F6 z+iZO;D|l6#EPqOiVnvdKh$vI&lC9frM}7C z@b~Ti@-n8j7S4IY)mT?gb`7vXC{Snzu+JR;e8}Kvg-A)US{&w*6?rY$$)oMpemTRe zMLn+m<$rDGkDqK_G-_DKp`y*0ynrYGgt5;6kjv@B#Km$)TWiH~u2ZY&BfzE~5!{bG%N)`|ks1&%##w{$-$m^7}dwB&Q-cG`Qwvfnwt zIzT-w$uf{7r#3BXZeZ5>V&aNOOw(A@i9zptT&zu-`>yrq##PR$@5`{$=IGp;lMO|G zKYvw7irjwkT&4tBAl0E=eO~nOw?d5BjsFWW(d{}V7x2J;F zl$DD^Mkle4N-j5my{F;xIU+-ca_yHiEmMdaH-T`h<-%fO$nJn*ZfUXX&0b(VQn=bt zarl7pu<5QiG&v0`NFziJkJn6^zmzjXgn!8m*Q>D0?f||FBUOY{2OcU^N;u4G0z5zo zk^WjTq99`3B6@T>P{6Fb98KE%C2E!Dk7I8%KphB>rTvleu(4K5OrHWn0!%?UUwz}A zvS>LJHCiztB?(Jz$~AD`tUSv{k4y$uS1_3&XaHP*$Pc8uvUly6m2TOYHCt}E(|<$; z4iMrdOyFZjCn1*$NKH3xcNNb_x0EcLsm3t(GzqaUK}6e5HbS}}klpIs)u+GfEKE(X z9pA7{v359=_SOztY5wxA4HFZk+6$FXqchN!5j`*g_qBZA6tDvi0iaOw3HvmNB$wp; zX~!1#AE%B1+2um*&CMR6u~9s5_YD@}xaI2aNiH`Mqd+!M$ZAmWB7!Blm0f}I@0>l7bsXKQD-MfCN zCsq3)LMkcb0AL)FK2 z^C^o~16_D16|m1i#H7pwMx1qN|4#R=jA6>9gZq%98(=ZepHL&9?oLUn+_jy&mmps` zRDy{ukc_-Kk$CmJKpm&-E0}<7kInKAYu<;|NA~g4*6qZSy&IJm<$q#xBeNvbg@quR z7UY1$vSUC1^w5E*`ibHRr})9G8u{>Blb`ld5MV^YNH{rf8xgIJMlley`T+#+j5w*u zN#HPH(M+Hn3Kf3NklbVLtAj0(mPU=516oqcpE9Df17?!ha^W z+|;7H1c7DF`7C67bVj}Q90GIg@i-p-k`Wa2>rNJEcGY*mxL#XrbI(MSr3o|`h?|FW zS{P?)9P=h!u+p%g$C_z>)tf44ih{Q3+jQg;?4KE=k_4JH7e~8{8OkNF*irAib=aa9 ztDP>ZOBEcn(H)aQ#)KYQRK$E#rhf_^#S~6?l2H(hIB$8}ktT?YI;&oz3%{}Z)xk@@I&xZmkk-+##z%_TEskU z2IG-W89~8Qck{OOCYO9IcIJzg=4tCiRBF|iu|s)cN1BNmHBq%$Uv9x4w0IvYaa(n1 if-0R}xnjj^tG@wES0N5rXV3Eh0000u@AA9_b|zNJuE^%!Me7ctReDR-AP*q{^Wp)rJ6^xe(c$s6``l`60-wQphPu z{((xB4p_!ngU*D%GEWQ&Qdr4;U<0)ADzp-)YAnexByDjrMvgUe2J;kDJDTo?IwV1K z*Cn8Ol0j&_lVLf%o->#pf})-gc$I%DSOA<`3I%u+uo&PykdlKQ0#Bro%a{C82ni#1 z`j`ucgR=(}l@9`Fw^<1kvrZHR&ZO5fxdsSG-U0xrHyKQ%i-snW%|%PVSb&*L?6*mv zR;#SGzrV``2_TMtRZY-s04~(LZi5Aqxq1Un@=(YnO=f{M33`K@H5&)q%n6C;MdSSs zvTCjb#AsxA7V?6NWPs63Lc!oBUAYE<)Ej}D=K;^SUA)%Ku?FD^MFo|vF8+R0z_;To zzlg_rg5!bK>sA72MTPFa3gMWa7?h}|~BS#|iApNdv|P?p(olXrKsfNowF+BV_E>W|LtTC?V2 ziVu3-n+q$){JC-a_VcZ6HEH&&lq1QG^3_GVZO4D};ic@(*-3fU zPr3=+#?jrrUU<4*I^u~&41u+gnU!>gO7+l0&`~UUo&?{+yd#9 z-C2#JTf4B+jf;h-=X`Uvy+nUIS~nI`ZcZA!Z{?tOFKp}El*FYqw7k0N%dN!?U(L-b z3t+Aj89kl3QN_Du99Dc-x3@j@+QD<}n}56{wB0#Wg?$UgwnXP`x_fElmD%Se?>|#& zIJlubsX(S&hR#NL%#Mz(7LfFLd%?KjR|6f)JNEq}Y5+9-fMtF^zqlav_VOEN*B3x< z4PE)!vcbWqdtAZgU}jFQ>DtlypHDX3@VsI3weC>I=S>-IyXU)>U*y;@vb#0&{XXNA zzWdnOt9NsH{+@I4|BY?aA9km*?acYF~5v%o1D6Yc)&$0pJAQ Ai2wiq diff --git a/src-tauri/resources/icons/tray-32x32-white.png b/src-tauri/resources/icons/tray-32x32-white.png index e5ede2fada84e0a5629b644aa054ed8d73a54803..00dea76a7b9b8ff462e66039203a115f18732fdb 100644 GIT binary patch delta 467 zcmV;^0WAKW5cLC)83+Ub007x@vVV~w7k>e#NklwRz@2sORgvs%9Sf_xOHJmq@_`#<^nCLlZP^*bS~J=EH|RfIp^iy z|K?=d#qW32df)u|e(T-8|L^nrWMqOk=)9)6HW^_YQ*z7VSV3JvdeVdUT4DrQ+kf11 zSBDz$0;^my2EMwWS$B+Jp#z?SLIP#O`ojh7Zu*HSgLc~Gk!c7Ov={-SdM!;S4NmzG zE?6s%0UKs~)nKTgO&(ol&U@knhi3<=Izf9vda}=?(Kx|!Cw=nKam!?6?R8yKsNkrr z9w>ro19r%!`KS#QJTr!p7YSvIyMJtTx(^hcoY5) zawbsp*b^lbqb2figx_R_=HT5ogBcR)~Z)ynW% zuTZPgP{JNHp^Y?~1`VldyPyuB(o_0uy>E+(#EgpX7={o-RFIt z=l8L{T~|^(D?TnI4ndIk!UD@Yct+LZ(b4cwaaJxJ*8!%xZUKXy^+XRM&AWEePo2d$-c54Z0 zmV5wZG!)L0B!wE_N|!|&8RjXJCTT4}W)ZX=r%9HiSiK$%|1c=$6CCV3OMX}kUYRhb zqIg+?@caE5zg8ppDhP^U7=ok;n#Lglmuox<7r;Go`Urys$h=SVDx%~;RYtB{s#Z)G zv^pGv+Z&5P-S0S|b{F_2PC%BxEIT1q>sckh`K~Ta6bG^MEW>`*@JI0@_Shqw%t4AK(

yUhI4hAf`xe7pdJrx)lNh52PT#^q8 zLwDMc3#*ka^vDY5;X$Frgu$3KqA0)?%gSOXPK!HeN{8zNogOczjU+A@7)O?#6KEZ9 zjO@2ae6?Cxb^mae3la}G{#6Bn!Jz}Z9_IzE0nc*iNZeS?aJa$9IrKcIqZl&6HpeHz z7mah>&Z@c+AS0tCjanVe;JlF`aRbl++(_!maji~E!bf@ls4TUcLk+?fh%zi)P5Awo z530f|mxzXXg5`L%*G(9&78wXw__lbLd_0tLc)#BXAnARIKddfG4#m&;z|0Dmy9YKG zaWC^Sx8nZRAFh-Q0{sj39#~bBw^CDo)ZVb9Yt7l0%Mwl;4aT%3&A67?T$g9q#4(z8 z&JN6-{LG}<=C3#IikZ~R#P_Y6!61(FVEVrL&+2|`|8-l`;)(TE{q(Dc$8_w|BHvO6lwQqPf&sUKAF1rt`SU*jCLJ9s{3Ltkn?(ed=My!K*a^mS_B`OXWs z#-paDIou!9e(KqOd0W(*RfhJVm3{S9U2hNeHO4Y2&2h=+#^ucVjo5YUrL8-i-(QU0 zURI==zOT!?tYL@s^3>Aq)0_63%Q!Rsa?{4L#XY;T7c4bi2=;bgJoIkoxcZM%Jq2x> z9w`x#_LJAL+Z(PAR=18WnRaXJ_^LE*<-R{-k2iH=!Is4i^$T8@S8Un&Y}x9608cK} ANB{r; diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index 71914f51..22f1d767 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -5,7 +5,8 @@ "resources-macos/binaries/*" ], "resources": [ - "resources-macos/resources/*" + "resources-macos/resources/*", + "resources/*" ] } } From f0f99c034cd0b0dda899a85c6b44863f4dc60c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:40:58 +0100 Subject: [PATCH 25/45] fix carousel folding (#150) --- .../client/pages/CarouselPage/style.scss | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/pages/client/pages/CarouselPage/style.scss b/src/pages/client/pages/CarouselPage/style.scss index fec9378b..df863b8d 100644 --- a/src/pages/client/pages/CarouselPage/style.scss +++ b/src/pages/client/pages/CarouselPage/style.scss @@ -33,7 +33,7 @@ text-align: center; width: 100%; font-weight: 400; - height: 72px; + min-height: 72px; } strong, @@ -46,9 +46,12 @@ display: grid; grid-template-rows: auto auto; grid-template-columns: 1fr; + align-items: center; + justify-items: center; row-gap: 20px; + column-gap: 10px; - @include media-breakpoint-up(xl) { + @include media-breakpoint-up(xxl) { grid-template-rows: auto; grid-template-columns: 1fr 1fr; column-gap: 40px; @@ -100,7 +103,8 @@ #welcome-slide { h2 { - margin-bottom: 40px; + padding-bottom: 40px; + display: block; } & > .row { @@ -167,11 +171,20 @@ } } + #factor-slide, + #security-slide, + #instances-slide, + #support-slide, + #welcome-slide { + min-height: 750px; + } + #factor-slide, #security-slide, #instances-slide, #support-slide { justify-content: space-between; + row-gap: 25px; } #support-slide { From a3cc4cef8f07e837ae407ec5c35875ea3f088f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:28:34 +0100 Subject: [PATCH 26/45] fix: app shudowm without cleanup (#151) style: add instance and tunnel layout feat: welcome carousel autoslide in time --- src-tauri/src/appstate.rs | 1 + src-tauri/src/tray.rs | 2 +- .../pages/CarouselPage/CarouselPage.tsx | 2 +- .../components/CardCarousel/CardCarousel.tsx | 43 ++++++++++++++++++- .../client/pages/CarouselPage/style.scss | 39 +++++++++-------- .../pages/ClientAddInstancePage/style.scss | 22 ++++++---- .../AddTunnelFormCard/AddTunnelFormCard.tsx | 2 + .../components/AddTunnelFormCard/style.scss | 25 +++++++++++ .../pages/ClientAddTunnelPage/style.scss | 41 +++++++++--------- 9 files changed, 125 insertions(+), 52 deletions(-) diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index 97882d62..2aa651e4 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -101,6 +101,7 @@ impl AppState { } Ok(()) } + pub fn find_connection( &self, id: i64, diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 8fc8dfd7..4e76d523 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -51,7 +51,7 @@ pub fn handle_tray_event(app: &AppHandle, event: SystemTrayEvent) { tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async { let _ = app_state.close_all_connections().await; - std::process::exit(0); + app.exit(0); }); }); } diff --git a/src/pages/client/pages/CarouselPage/CarouselPage.tsx b/src/pages/client/pages/CarouselPage/CarouselPage.tsx index c749905b..1a823276 100644 --- a/src/pages/client/pages/CarouselPage/CarouselPage.tsx +++ b/src/pages/client/pages/CarouselPage/CarouselPage.tsx @@ -46,7 +46,7 @@ export const CarouselPage = () => { return (

); }; diff --git a/src/pages/client/pages/CarouselPage/components/CardCarousel/CardCarousel.tsx b/src/pages/client/pages/CarouselPage/components/CardCarousel/CardCarousel.tsx index 21ca97fe..c2a50014 100644 --- a/src/pages/client/pages/CarouselPage/components/CardCarousel/CardCarousel.tsx +++ b/src/pages/client/pages/CarouselPage/components/CardCarousel/CardCarousel.tsx @@ -3,7 +3,8 @@ import './style.scss'; import classNames from 'classnames'; import { AnimatePresence, motion } from 'framer-motion'; import { isUndefined } from 'lodash-es'; -import { HTMLProps, useMemo, useState } from 'react'; +import { HTMLProps, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { interval } from 'rxjs'; import { CarouselControls } from './components/CarouselControl/CarouselControl'; import { CarouselItem } from './types'; @@ -12,6 +13,10 @@ type Props = { cards: CarouselItem[]; activeCardIndex?: number; onChange?: (index: number) => void; + /** Progress slides if main container is not hovered, this can only be used when activeCardIndex is NOT provided */ + autoSlide?: boolean; + /** How often carousel will change, in milisenconds*/ + autoSlideInterval?: number; } & HTMLProps; export const CardCarousel = ({ @@ -19,8 +24,11 @@ export const CardCarousel = ({ cards, activeCardIndex, onChange, + autoSlide = false, + autoSlideInterval = 4000, ...rest }: Props) => { + const hoveredRef = useRef(false); const [internalIndex, setInternalIndex] = useState(0); const cardsCount = useMemo(() => cards.length, [cards.length]); @@ -36,8 +44,39 @@ export const CardCarousel = ({ return cards[activeIndex]; }, [activeIndex, cards]); + const nextSlide = useCallback(() => { + setInternalIndex((currentIndex) => { + if (currentIndex === cardsCount - 1) { + return 0; + } + return currentIndex + 1; + }); + }, [setInternalIndex, cardsCount]); + + useEffect(() => { + if (autoSlide) { + const sub = interval(autoSlideInterval).subscribe(() => { + if (!hoveredRef.current) { + nextSlide(); + } + }); + return () => { + sub.unsubscribe(); + }; + } + }, [nextSlide, autoSlide, autoSlideInterval]); + return ( -
+
{ + hoveredRef.current = true; + }} + onMouseLeave={() => { + hoveredRef.current = false; + }} + {...rest} + > .content { - display: flex; - flex-flow: row wrap; - align-items: flex-start; - justify-content: center; - row-gap: 50px; - column-gap: 50px; + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); + align-items: start; + justify-items: center; + column-gap: 25px; + row-gap: 25px; - @include media-breakpoint-up(xxl) { - justify-content: flex-start; + @include media-breakpoint-up(xl) { + column-gap: 50px; } & > * { width: 100%; - max-width: 700px; + flex-grow: 1; } & > .card { box-sizing: border-box; padding: 32px 64px; + & > h2 { width: 100%; text-align: center; padding-bottom: 42px; } + form > .controls { padding-top: 42px; diff --git a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx index bf01a225..cee46294 100644 --- a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx +++ b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx @@ -1,3 +1,5 @@ +import './style.scss'; + import { zodResolver } from '@hookform/resolvers/zod'; import { useEffect, useMemo, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; diff --git a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss index e69de29b..82629d61 100644 --- a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss +++ b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss @@ -0,0 +1,25 @@ +@use '@scssutils' as *; + +#add-tunnel-form-card { + & > header { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: flex-start; + padding-bottom: 10px; + gap: 10px; + + h2 { + text-wrap: nowrap; + } + + .controls { + margin-left: auto; + display: flex; + flex-flow: row nowrap; + gap: 10px; + align-items: center; + justify-content: flex-start; + } + } +} diff --git a/src/pages/client/pages/ClientAddTunnelPage/style.scss b/src/pages/client/pages/ClientAddTunnelPage/style.scss index 8d3d8002..96f01a35 100644 --- a/src/pages/client/pages/ClientAddTunnelPage/style.scss +++ b/src/pages/client/pages/ClientAddTunnelPage/style.scss @@ -10,6 +10,7 @@ @include typography(app-body-1); color: var(--text-body-primary); } + h3 { @include typography(app-side-bar); color: var(--text-body-primary); @@ -25,6 +26,7 @@ flex-flow: row; align-items: center; justify-content: center; + .btn { width: 100%; max-width: 200px; @@ -46,65 +48,63 @@ } & > .content { - display: flex; - flex-flow: row wrap; - align-items: flex-start; - justify-content: center; - row-gap: 50px; - column-gap: 50px; - - @include media-breakpoint-up(xxl) { - justify-content: flex-start; + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); + align-items: start; + justify-items: center; + column-gap: 25px; + row-gap: 25px; + + @include media-breakpoint-up(xl) { + column-gap: 50px; } & > * { width: 100%; - max-width: 700px; + flex-grow: 1; } & > .card { box-sizing: border-box; padding: 32px 64px; - & > header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - & > .controls { - display: flex; - gap: 15px; - } - } + form { & > .client { border-bottom: 1px solid var(--border-primary); margin-bottom: 10px; } + & > h3 { margin-bottom: 10px; } + .advanced-options-header { display: flex; align-items: center; gap: 5px; margin-bottom: 10px; + & > button { background: none; border: none; padding: 0; margin: 0; } + .arrow-single { width: 22px; height: 22px; margin-left: auto; } + .underscore { flex-grow: 1; border-bottom: 1px solid var(--border-primary); margin-right: 10px; } } + .advanced-options { display: none; transition: opacity 0.5s ease; @@ -113,6 +113,7 @@ .advanced-options.open { display: block; } + > .controls { padding-top: 42px; From aad9206e65b525ac7888c8bf332ab0bc18456349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:01:11 +0100 Subject: [PATCH 27/45] fix: client nav tunnel items (#152) --- src/components/App/App.tsx | 4 + .../ClientSideBar/ClientSideBar.tsx | 16 +++- .../ClientBarItem/ClientBarItem.tsx | 80 +++++++++++-------- .../AddTunnelFormCard/AddTunnelFormCard.tsx | 13 ++- .../components/AddTunnelFormCard/style.scss | 9 +++ .../ClientInstancePage/ClientInstancePage.tsx | 51 +++++++----- src/shared/routes.ts | 2 +- 7 files changed, 115 insertions(+), 60 deletions(-) diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index c34f1175..478edbeb 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -69,6 +69,10 @@ const router = createBrowserRouter([ { path: '/client/', index: true, + element: , + }, + { + path: '/client/instance', element: , }, { diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index f79675bc..07ded53a 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -50,8 +50,13 @@ export const ClientSideBar = () => {
{instances.map((instance) => ( ))} @@ -78,8 +83,11 @@ export const ClientSideBar = () => {
{tunnels.map((tunnel) => ( ))} diff --git a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx index 2371b99f..969ecd57 100644 --- a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx +++ b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx @@ -1,5 +1,7 @@ import { autoUpdate, useFloating } from '@floating-ui/react'; import classNames from 'classnames'; +import { isUndefined } from 'lodash-es'; +import { useMemo } from 'react'; import { useMatch, useNavigate } from 'react-router-dom'; import SvgIconConnection from '../../../../../../shared/defguard-ui/components/svg/IconConnection'; @@ -7,30 +9,35 @@ import { routes } from '../../../../../../shared/routes'; import { useClientStore } from '../../../../hooks/useClientStore'; import { WireguardInstanceType } from '../../../../types'; -interface BaseInstance { - id?: number; - name: string; - // Connected - active: boolean; - type: WireguardInstanceType; -} - -type Props = { - instance: T; +type Props = { + itemType: WireguardInstanceType; + itemId: number; + label: string; + active?: boolean; }; -export const ClientBarItem = ({ instance }: Props) => { - const instancePage = useMatch('/client/'); +export const ClientBarItem = ({ + itemType, + itemId, + label, + active: acitve = false, +}: Props) => { + const instancePage = useMatch('/client/instance/'); const navigate = useNavigate(); const setClientStore = useClientStore((state) => state.setState); const selectedInstance = useClientStore((state) => state.selectedInstance); - - const active = - instance.type === selectedInstance?.type && instance.id === selectedInstance.id; + const itemSelected = useMemo(() => { + return ( + !isUndefined(selectedInstance) && + !isUndefined(selectedInstance?.id) && + selectedInstance.id === itemId && + selectedInstance.type === itemType + ); + }, [selectedInstance, itemType, itemId]); const cn = classNames('client-bar-item', 'clickable', { - active: active, - connected: instance.active, + active: itemSelected, + connected: acitve, }); const { refs, floatingStyles } = useFloating({ @@ -45,34 +52,37 @@ export const ClientBarItem = ({ instance }: Props) => className={cn} ref={refs.setReference} onClick={() => { - if (instance.type === WireguardInstanceType.DEFGUARD_INSTANCE) { - setClientStore({ - selectedInstance: { - id: instance.id as number, - type: WireguardInstanceType.DEFGUARD_INSTANCE, - }, - }); - } else { - setClientStore({ - selectedInstance: { - id: instance.id as number, - type: WireguardInstanceType.TUNNEL, - }, - }); + switch (itemType) { + case WireguardInstanceType.DEFGUARD_INSTANCE: + setClientStore({ + selectedInstance: { + id: itemId, + type: WireguardInstanceType.DEFGUARD_INSTANCE, + }, + }); + break; + case WireguardInstanceType.TUNNEL: + setClientStore({ + selectedInstance: { + id: itemId, + type: WireguardInstanceType.TUNNEL, + }, + }); + break; } if (!instancePage) { - navigate(routes.client.base, { replace: true }); + navigate(routes.client.instancePage, { replace: true }); } }} > -

{instance.name}

+

{label}

-

{instance.name[0]}

+

{label[0]}

- {instance.active && ( + {acitve && (
{ dns: z .string() .refine((value) => { - if (value) { + if (value && value.length != 0) { return validateIpOrDomainList(value, ',', true); } return true; @@ -160,7 +161,15 @@ export const AddTunnelFormCard = () => { if (reader.result && input.files) { const res = reader.result; parseTunnelConfig(res as string) - .then((data) => reset(data as FormFields)) + .then((data) => { + const fileData = data as Partial; + const trimed = pickBy( + fileData, + (value) => value !== undefined && value !== null, + ); + const parsedConfig = { ...defaultValues, ...trimed }; + reset(parsedConfig); + }) .catch(() => toaster.error(localLL.messages.configError())); } }; diff --git a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss index 82629d61..be3a2aa7 100644 --- a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss +++ b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss @@ -20,6 +20,15 @@ gap: 10px; align-items: center; justify-content: flex-start; + + & > .btn { + min-width: 135px; + + span { + display: block; + padding: 0 1px; + } + } } } } diff --git a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx index cc8c0db0..526bb262 100644 --- a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx +++ b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx @@ -1,5 +1,6 @@ import './style.scss'; +import { isUndefined } from 'lodash-es'; import { useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -8,7 +9,7 @@ import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/ import { ButtonStyleVariant } from '../../../../shared/defguard-ui/components/Layout/Button/types'; import { routes } from '../../../../shared/routes'; import { useClientStore } from '../../hooks/useClientStore'; -import { WireguardInstanceType } from '../../types'; +import { DefguardInstance, WireguardInstanceType } from '../../types'; import { LocationsList } from './components/LocationsList/LocationsList'; import { StatsFilterSelect } from './components/StatsFilterSelect/StatsFilterSelect'; import { StatsLayoutSelect } from './components/StatsLayoutSelect/StatsLayoutSelect'; @@ -21,26 +22,38 @@ export const ClientInstancePage = () => { const instanceLL = LL.pages.client.pages.instancePage; const tunelLL = LL.pages.client.pages.tunnelPage; const instances = useClientStore((state) => state.instances); + const tunnels = useClientStore((state) => state.tunnels); const [selectedInstanceId, selectedInstanceType] = useClientStore((state) => [ state.selectedInstance?.id, state.selectedInstance?.type, ]); - const selectedInstance = useMemo( - () => instances.find((i) => i.id === selectedInstanceId), - [instances, selectedInstanceId], - ); + + const selectedInstance = useMemo((): DefguardInstance | undefined => { + if ( + !isUndefined(selectedInstanceId) && + selectedInstanceType && + selectedInstanceType === WireguardInstanceType.DEFGUARD_INSTANCE + ) { + return instances.find((i) => i.id === selectedInstanceId); + } + }, [selectedInstanceId, selectedInstanceType, instances]); + const navigate = useNavigate(); const isLocationPage = selectedInstanceType === WireguardInstanceType.DEFGUARD_INSTANCE; const openUpdateInstanceModal = useUpdateInstanceModal((state) => state.open); - // router guard, if no instances redirect to add instance, for now, later this will be replaced by init welcome flow useEffect(() => { - if (instances.length === 0) { + if ( + !selectedInstanceType || + (selectedInstanceType === WireguardInstanceType.DEFGUARD_INSTANCE && + !selectedInstance) || + (selectedInstanceType === WireguardInstanceType.TUNNEL && tunnels.length === 0) + ) { navigate(routes.client.addInstance, { replace: true }); } - }, [instances, navigate]); + }, [selectedInstance, navigate, tunnels.length, selectedInstanceType]); return (
@@ -51,16 +64,18 @@ export const ClientInstancePage = () => { {isLocationPage && ( <> -
); }; +const FooterApplicationInfo = () => { + const { LL } = useI18nContext(); + const [appVersion, setAppVersion] = useState('-'); + + useEffect(() => { + const getAppVersion = async () => { + const version = await getVersion().catch(() => { + return ''; + }); + setAppVersion(version); + }; + + getAppVersion(); + }, []); + + return ( + + ); +}; + const SettingsNav = () => { const { LL } = useI18nContext(); const navigate = useNavigate(); diff --git a/src/pages/client/components/ClientSideBar/components/NewApplicationVersionAvailableInfo/NewApplicationVersionAvailableInfo.tsx b/src/pages/client/components/ClientSideBar/components/NewApplicationVersionAvailableInfo/NewApplicationVersionAvailableInfo.tsx new file mode 100644 index 00000000..1d7d87b8 --- /dev/null +++ b/src/pages/client/components/ClientSideBar/components/NewApplicationVersionAvailableInfo/NewApplicationVersionAvailableInfo.tsx @@ -0,0 +1,81 @@ +import './style.scss'; + +import { shallow } from 'zustand/shallow'; + +import { useApplicationUpdateStore } from '../../../../../../components/ApplicationUpdateManager/useApplicationUpdateStore'; +import { useNewAppVersionAvailable } from '../../../../../../components/ApplicationUpdateManager/useNewAppVersionAvailable'; +import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { clientApi } from '../../../../../../pages/client/clientAPI/clientApi'; +import SvgIconDownload from '../../../../../../shared/defguard-ui/components/svg/IconDownload'; +import { useClientStore } from '../../../../../client/hooks/useClientStore'; + +const { openLink } = clientApi; + +export const NewApplicationVersionAvailableInfo = () => { + const { LL } = useI18nContext(); + const { newAppVersionAvailable } = useNewAppVersionAvailable(); + const checkForUpdates = useClientStore((state) => state.settings.check_for_updates); + + const dismissed = useApplicationUpdateStore((state) => state.dismissed, shallow); + const setValues = useApplicationUpdateStore((state) => state.setValues, shallow); + + const [latestVersion, releaseDate, releaseNotesUrl, updateUrl] = + useApplicationUpdateStore( + (state) => [ + state.latestVersion, + state.releaseDate, + state.releaseNotesUrl, + state.updateUrl, + ], + shallow, + ); + + if ( + dismissed || + !checkForUpdates || + !newAppVersionAvailable || + !latestVersion || + !releaseDate || + !releaseNotesUrl || + !updateUrl + ) + return null; + + return ( +
+
+

+ {LL.pages.client.newApplicationVersion.header()} {latestVersion} +

+ openLink(updateUrl)} + /> +
+
+

setValues({ dismissed: true })}> + {LL.pages.client.newApplicationVersion.dismiss()} +

+

openLink(releaseNotesUrl)}> + {LL.pages.client.newApplicationVersion.releaseNotes()} +

+
+
+

{LL.pages.client.newApplicationVersion.header()}

+

{latestVersion}

+ openLink(updateUrl)} + /> +
+

openLink(releaseNotesUrl)}> + {LL.pages.client.newApplicationVersion.releaseNotes()} +

+

setValues({ dismissed: true })}> + {LL.pages.client.newApplicationVersion.dismiss()} +

+
+
+
+ ); +}; diff --git a/src/pages/client/components/ClientSideBar/components/NewApplicationVersionAvailableInfo/style.scss b/src/pages/client/components/ClientSideBar/components/NewApplicationVersionAvailableInfo/style.scss new file mode 100644 index 00000000..964b7a11 --- /dev/null +++ b/src/pages/client/components/ClientSideBar/components/NewApplicationVersionAvailableInfo/style.scss @@ -0,0 +1,86 @@ +@use '@scssutils' as *; + +#settings-new-application-version-available { + flex-direction: column; + width: 100%; + background-color: var(--surface-frame-bg); + + @include media-breakpoint-down(lg) { + background-color: transparent; + } + + & > .new-version-header { + padding: 20px; + padding-bottom: 0; + display: flex; + justify-content: space-between; + align-items: center; + + @include media-breakpoint-down(lg) { + display: none; + } + + & > .new-version-download-icon { + cursor: pointer; + } + + & > h3 { + margin-right: 5px; + @include typography(markdown-h6); + color: var(--text-body-primary); + } + } + + & > .new-version-subheader { + display: flex; + padding: 20px; + padding-top: 8px; + justify-content: space-between; + color: var(--text-body-primary); + font-weight: 300; + font-size: 14px; + + @include media-breakpoint-down(lg) { + display: none; + } + + & > p { + cursor: pointer; + @include typography(app-copyright); + font-size: 12px; + } + } + + & > .settings-new-application-version-mobile { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; + background-color: var(--surface-frame-bg); + padding-top: 10px; + padding-bottom: 5px; + + @include media-breakpoint-up(lg) { + display: none; + } + + & > p, + & > div > p { + text-align: center; + @include typography(app-copyright); + color: var(--text-body-primary); + } + + & > svg { + height: 32px; + width: 32px; + cursor: pointer; + } + + & > div > p { + cursor: pointer; + line-height: 11px; + margin-bottom: 5px; + } + } +} diff --git a/src/pages/client/components/ClientSideBar/style.scss b/src/pages/client/components/ClientSideBar/style.scss index 62ade847..e463ed24 100644 --- a/src/pages/client/components/ClientSideBar/style.scss +++ b/src/pages/client/components/ClientSideBar/style.scss @@ -86,7 +86,8 @@ padding-top: 70px; } - & > .client-bar-item { + & > .client-bar-item, + & > div > .client-bar-item { display: grid; box-sizing: border-box; width: 100%; @@ -109,7 +110,7 @@ & > svg, & > .icon-wrapper { - display: none; + margin-bottom: 20px; grid-column: 1; grid-row: 1; width: 40px; @@ -119,6 +120,7 @@ display: flex; width: 24px; height: 24px; + margin-bottom: 0; } } @@ -222,7 +224,7 @@ } #settings-nav-item { - margin-top: auto; + // margin-top: auto; } #add-instance { @@ -263,3 +265,31 @@ content: ' '; z-index: 3; } + +#footer-application-info { + width: 100%; + padding-top: 20px; + padding-bottom: 20px; + + & > p { + @include typography(app-copyright); + color: var(--text-body-tertiary); + text-align: center; + + @include media-breakpoint-down(lg) { + padding-left: 5px; + padding-right: 5px; + } + + & > span { + cursor: pointer; + } + } +} + +.client-bar-bottom-menu-container { + display: flex; + flex-direction: column; + width: 100%; + margin-top: auto; +} diff --git a/src/pages/client/hooks/useClientStore.tsx b/src/pages/client/hooks/useClientStore.tsx index a501f8bd..4ce69add 100644 --- a/src/pages/client/hooks/useClientStore.tsx +++ b/src/pages/client/hooks/useClientStore.tsx @@ -24,6 +24,7 @@ const defaultValues: StoreValues = { log_level: 'error', theme: 'light', tray_icon_theme: 'color', + check_for_updates: true, }, }; diff --git a/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx b/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx index e4f80206..493e41ea 100644 --- a/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx +++ b/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx @@ -4,6 +4,7 @@ import { useMutation } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { LabeledCheckbox } from '../../../../../../shared/defguard-ui/components/Layout/LabeledCheckbox/LabeledCheckbox'; import { Select } from '../../../../../../shared/defguard-ui/components/Layout/Select/Select'; import { SelectOption, @@ -20,6 +21,10 @@ export const GlobalSettingsTab = () => { return (
+
+

{localLL.versionUpdate.title()}

+ +

{localLL.tray.title()}

@@ -219,3 +224,24 @@ const TrayIconThemeSelect = () => { /> ); }; + +const CheckForUpdatesOption = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.settingsPage.tabs.global; + const settings = useClientStore((state) => state.settings); + const updateClientSettings = useClientStore((state) => state.updateSettings); + const { mutate, isPending } = useMutation({ + mutationFn: updateClientSettings, + }); + + return ( + { + mutate({ check_for_updates: value }); + }} + /> + ); +}; diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts index a235a833..354edc5f 100644 --- a/src/pages/client/types.ts +++ b/src/pages/client/types.ts @@ -76,4 +76,5 @@ export enum TauriEventKey { CONNECTION_CHANGED = 'connection-changed', INSTANCE_UPDATE = 'instance-update', LOCATION_UPDATE = 'location-update', + APP_VERSION_FETCH = 'app-version-fetch', } diff --git a/src/shared/hooks/api/types.ts b/src/shared/hooks/api/types.ts index 6e462dd6..c6e478a1 100644 --- a/src/shared/hooks/api/types.ts +++ b/src/shared/hooks/api/types.ts @@ -91,6 +91,13 @@ export type EnrollmentInstanceInfo = { url: string; }; +export type NewApplicationVersionInfo = { + version: string; + release_date: string; + release_notes_url: string; + update_url: string; +}; + // FIXME: strong types export type UseApi = { enrollment: { From 73af79d471036c82d22820ed08256b0ade5cc2ef Mon Sep 17 00:00:00 2001 From: blazej-teonite <104985522+blazej-teonite@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:29:43 +0100 Subject: [PATCH 32/45] Set version to 0.2.0 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3bd05fb2..a1466af5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "defguard-client", "private": false, - "version": "0.1.1", + "version": "0.2.0", "type": "module", "scripts": { "dev": "npm-run-all --parallel vite typesafe-i18n", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 99caa8de..174d7b43 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1140,7 +1140,7 @@ checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" [[package]] name = "defguard-client" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", "base64 0.21.6", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 199e01a5..273d7bbe 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard-client" -version = "0.1.1" +version = "0.2.0" description = "Defguard desktop client" license = "Apache-2.0" homepage = "https://github.com/DefGuard/client" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6ff2b59c..52df3b66 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "defguard-client", - "version": "0.1.1" + "version": "0.2.0" }, "tauri": { "systemTray": { From efd4ced5684657029039302b43f9a53d3ac0bd84 Mon Sep 17 00:00:00 2001 From: blazej-teonite <104985522+blazej-teonite@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:16:23 +0100 Subject: [PATCH 33/45] Fix log and chart display. Copy to clipboard, logs download (#163) * Fix log and chart display. Copy to clipboard, logs download --- src-tauri/Cargo.lock | 192 ++++++++++++++++++ src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 6 + .../components/LocationCardInfo/style.scss | 3 +- .../LocationConnectionHistory.tsx | 6 +- .../LocationConnectionHistory/style.scss | 14 +- .../LocationDetailCard/LocationDetailCard.tsx | 10 +- .../components/LocationDetailCard/style.scss | 11 +- .../components/LocationLogs/LocationLogs.tsx | 28 ++- 9 files changed, 247 insertions(+), 25 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 174d7b43..47261b9f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -140,6 +140,25 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +[[package]] +name = "arboard" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08" +dependencies = [ + "clipboard-win", + "core-graphics", + "image", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot", + "thiserror", + "winapi", + "x11rb", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -791,6 +810,17 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "cocoa" version = "0.24.1" @@ -1449,6 +1479,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -1877,6 +1917,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -2348,6 +2398,8 @@ dependencies = [ "color_quant", "num-rational", "num-traits", + "png", + "tiff", ] [[package]] @@ -2525,6 +2577,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.66" @@ -3187,6 +3245,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -4052,6 +4121,30 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + [[package]] name = "rkyv" version = "0.7.43" @@ -4802,6 +4895,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "string_cache" version = "0.8.7" @@ -5091,6 +5190,7 @@ dependencies = [ "rand 0.8.5", "raw-window-handle", "reqwest", + "rfd", "semver", "serde", "serde_json", @@ -5226,6 +5326,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158" dependencies = [ + "arboard", "cocoa", "gtk", "percent-encoding", @@ -5340,6 +5441,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.31" @@ -6091,6 +6203,12 @@ dependencies = [ "windows-metadata", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "which" version = "4.4.2" @@ -6140,12 +6258,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + [[package]] name = "windows" version = "0.39.0" @@ -6320,6 +6460,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + [[package]] name = "windows_aarch64_msvc" version = "0.39.0" @@ -6344,6 +6490,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + [[package]] name = "windows_i686_gnu" version = "0.39.0" @@ -6368,6 +6520,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + [[package]] name = "windows_i686_msvc" version = "0.39.0" @@ -6392,6 +6550,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + [[package]] name = "windows_x86_64_gnu" version = "0.39.0" @@ -6434,6 +6598,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + [[package]] name = "windows_x86_64_msvc" version = "0.39.0" @@ -6564,6 +6734,28 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +dependencies = [ + "gethostname", + "nix 0.26.4", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" +dependencies = [ + "nix 0.26.4", +] + [[package]] name = "x25519-dalek" version = "2.0.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 273d7bbe..73021f4d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,7 +37,7 @@ strum = { version = "0.25", features = ["derive"] } dark-light = "1.0" webbrowser = "0.8" -tauri = { version = "1.5", features = [ "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png", "fs-all"] } +tauri = { version = "1.5", features = [ "dialog-all", "clipboard-all", "http-all", "window-all", "system-tray", "native-tls-vendored", "icon-png", "fs-all"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } thiserror = "1.0" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 52df3b66..22e25064 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -29,6 +29,12 @@ "fs": { "all": true, "scope": ["$RESOURCE/*", "$APPDATA/*"] + }, + "clipboard": { + "all": true + }, + "dialog": { + "all": true } }, "bundle": { diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/style.scss b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/style.scss index fc6b847b..8053c313 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/style.scss +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardInfo/style.scss @@ -2,7 +2,8 @@ .location-card-info-from, .location-card-info-connected, -.location-card-info-ip { +.location-card-info-ip, +.location-card-allowed-traffic { display: flex; flex-flow: column; align-items: flex-start; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx index a310cf2c..5e5b9801 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx @@ -42,7 +42,11 @@ export const LocationConnectionHistory = ({

{localLL.title()}

- {connectionHistory.length === 0 && !connected && } + {connectionHistory.length === 0 && !connected && ( +
+ +
+ )} {connectionHistory.length > 0 && ( )} diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/style.scss b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/style.scss index 3ac5a2b1..226a6f52 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/style.scss +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/style.scss @@ -21,10 +21,10 @@ } .connections-list { - height: 400px; + max-height: 400px; grid-template-rows: 28px 1fr; - margin-bottom: 50px; box-sizing: border-box; + margin-top: 20px; .headers { @include list-layout; @@ -36,11 +36,6 @@ margin-right: 5px; grid-row: 2; grid-column: 1; - padding-bottom: 15px; - - @include media-breakpoint-up(lg) { - padding-bottom: 40px; - } } .custom-row { @@ -63,4 +58,9 @@ } } } + + & > .location-never-connected { + margin-bottom: 30px; + margin-top: 10px; + } } diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx index 086db445..83ece7f2 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx @@ -91,14 +91,14 @@ export const LocationDetailCard = memo(({ location, tabbed = false }: Props) =>
)} {location?.active && ( - <> - +
+

{location.route_all_traffic ? localLL.controls.traffic.allTraffic() : localLL.controls.traffic.predefinedTraffic()}

- +
)}
)} @@ -151,7 +151,9 @@ export const LocationDetailCard = memo(({ location, tabbed = false }: Props) => /> )} {(!locationStats || locationStats.length == 0) && !location?.active && ( - +
+ +
)} ); diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/style.scss b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/style.scss index 18b0b967..6149cc70 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/style.scss +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/style.scss @@ -6,7 +6,6 @@ flex-flow: column; row-gap: 20px; padding: 20px 0; - min-height: 50vh; overflow: hidden; max-height: none; margin-bottom: 20px; @@ -15,10 +14,6 @@ border-top-left-radius: 0; } - @include media-breakpoint-up(lg) { - min-height: 600px; - } - & > .location-no-stats { padding-top: 50px; } @@ -126,5 +121,11 @@ justify-content: flex-start; } } + + & > .no-stats-container { + min-height: 25vh; + display: flex; + align-items: center; + } } } diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx index 9c5bbaf6..d40e7c39 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx @@ -25,14 +25,14 @@ export const LocationLogs = ({ locationId, connectionType }: Props) => { const logsContainerElement = useRef(null); const appLogLevel = useClientStore((state) => state.settings.log_level); const locationLogLevelRef = useRef(appLogLevel); - const logsRef = useRef(''); const { LL } = useI18nContext(); const localLL = LL.pages.client.pages.instancePage.detailView.details.logs; const handleLogsDownload = async () => { const path = await save({}); if (path) { - await writeTextFile(path, logsRef.current); + const logs = getAllLogs(); + await writeTextFile(path, logs); } }; @@ -78,6 +78,18 @@ export const LocationLogs = ({ locationId, connectionType }: Props) => { //eslint-disable-next-line }, [locationId]); + const getAllLogs = () => { + let logs = ''; + + if (logsContainerElement) { + logsContainerElement.current?.childNodes.forEach((item) => { + logs += item.textContent + '\n'; + }); + } + + return logs; + }; + return (
@@ -91,7 +103,10 @@ export const LocationLogs = ({ locationId, connectionType }: Props) => { { - clipboard.writeText(logsRef.current); + const logs = getAllLogs(); + if (logs) { + clipboard.writeText(logs); + } }} /> { // return true if log should be visible const filterLogByLevel = (target: LogLevel, log: LogLevel): boolean => { + const log_level = log.toLocaleLowerCase(); switch (target) { case 'error': - return log === 'error'; + return log_level === 'error'; case 'info': - return ['info', 'error'].includes(log); + return ['info', 'error'].includes(log_level); case 'debug': - return ['error', 'info', 'debug'].includes(log); + return ['error', 'info', 'debug'].includes(log_level); case 'trace': return true; } From 125dab29c818bef6e14918c4d1d307f6efd5e6cf Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Thu, 18 Jan 2024 12:58:23 +0100 Subject: [PATCH 34/45] fix: routing bug showing add instance when clicking on tunnels and incorrect allowed ips in details (#167) --- .../ClientInstancePage/ClientInstancePage.tsx | 15 ++++++++------- .../LocationDetails/LocationDetails.tsx | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx index 526bb262..701e3ecb 100644 --- a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx +++ b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx @@ -45,15 +45,16 @@ export const ClientInstancePage = () => { const openUpdateInstanceModal = useUpdateInstanceModal((state) => state.open); useEffect(() => { - if ( - !selectedInstanceType || - (selectedInstanceType === WireguardInstanceType.DEFGUARD_INSTANCE && - !selectedInstance) || - (selectedInstanceType === WireguardInstanceType.TUNNEL && tunnels.length === 0) - ) { + const isDefguardInstance = + selectedInstanceType === WireguardInstanceType.DEFGUARD_INSTANCE; + const isTunnelInstance = selectedInstanceType === WireguardInstanceType.TUNNEL; + + if (isDefguardInstance && !selectedInstance) { navigate(routes.client.addInstance, { replace: true }); + } else if (isTunnelInstance && tunnels.length === 0) { + navigate(routes.client.addTunnel, { replace: true }); } - }, [selectedInstance, navigate, tunnels.length, selectedInstanceType]); + }, [selectedInstance, selectedInstanceType, tunnels.length, navigate]); return (
diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx index a6b464cb..3c7f9abb 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx @@ -90,7 +90,7 @@ const InfoSection = memo(({ locationId, connectionType }: Props) => {
- {data && data.address.split(',').map((ip) =>

{ip}

)} + {data && data.allowed_ips.split(',').map((ip) =>

{ip}

)}
From 953db3dc889b3a501b477f3d8e26aed1ec671611 Mon Sep 17 00:00:00 2001 From: Maciek Date: Thu, 18 Jan 2024 16:46:40 +0100 Subject: [PATCH 35/45] fix: handle typical config file format in tunnel import (#168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update valid address pattern * add domain explanation to DNS hint * fix typos * remove search domain from DNS config * add a fixme for later --------- Co-authored-by: Maciej Wójcik --- src-tauri/src/lib.rs | 2 +- src-tauri/src/wg_config.rs | 11 ++++-- src/i18n/en/index.ts | 36 +++++++++---------- src/i18n/i18n-types.ts | 14 ++++---- .../CarouselPage/cards/CarouselCards.tsx | 4 +-- .../ClientInstancePage/ClientInstancePage.tsx | 4 +-- .../components/TimeLeft/TimeLeft.tsx | 4 +-- src/shared/patterns.ts | 2 +- 8 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f082c508..f1621a1e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -20,7 +20,7 @@ struct Payload { cwd: String, } -/// Location type used in commands to check if we using tunel or location +/// Location type used in commands to check if we using tunnel or location #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] pub enum ConnectionType { Tunnel, diff --git a/src-tauri/src/wg_config.rs b/src-tauri/src/wg_config.rs index e4c235f5..3a872b0f 100644 --- a/src-tauri/src/wg_config.rs +++ b/src-tauri/src/wg_config.rs @@ -51,7 +51,14 @@ pub fn parse_wireguard_config(config: &str) -> Result Some(address.to_string()), + None => Some(dns.to_string()), + }); let pre_up = interface_section.get("PreUp"); let post_up = interface_section.get("PostUp"); @@ -109,7 +116,7 @@ mod test { PrivateKey = GAA2X3DW0WakGVx+DsGjhDpTgg50s1MlmrLf24Psrlg= Address = 10.0.0.1/24 ListenPort = 55055 - DNS = 10.0.0.2 + DNS = 10.0.0.2, tnt, teonite.net PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT [Peer] diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 30855f42..6aa30f18 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -6,11 +6,11 @@ const en = { time: { seconds: { singular: 'second', - prular: 'seconds', + plural: 'seconds', }, minutes: { singular: 'minute', - prular: 'minutes', + plural: 'minutes', }, }, form: { @@ -66,8 +66,8 @@ const en = { subtitle: 'Establish a connection to defguard instance effortlessly by configuring it with a single token.', }, - tunel: { - title: 'Add Tunel', + tunnel: { + title: 'Add Tunnel', subtitle: 'Utilize it as a WireGuard® Desktop Client with ease. Set up your own tunnel or import a configuration file.', }, @@ -76,9 +76,9 @@ const en = { // md title: 'WireGuard **2FA with defguard**', // md - sideText: `Since Wireguard protocol doesn't support 2FA/MFA - most (if not all) currently available Wireguard clients do not support real Multi-Factor Authentication/2FA - and use 2FA just as authorization to the "application" itself (and not Wireguard tunnel). + sideText: `Since WireGuard protocol doesn't support 2FA/MFA - most (if not all) currently available WireGuard clients do not support real Multi-Factor Authentication/2FA - and use 2FA just as authorization to the "application" itself (and not WireGuard tunnel). -If you would like to secure your Wireguard instance try **defguard** VPN & SSO server (which is also free & open source) to get real 2FA using Wireguard PSK keys and peers configuration by defguard gateway!`, +If you would like to secure your WireGuard instance try **defguard** VPN & SSO server (which is also free & open source) to get real 2FA using WireGuard PSK keys and peers configuration by defguard gateway!`, }, security: { // md @@ -236,7 +236,7 @@ If you are an admin/devops - all your customers (instances) and all their tunnel serverAddress: 'Server Address', allowedIps: 'Allowed IPs', dns: 'DNS servers', - keepalive: 'Persisten keepalive', + keepalive: 'Persistent keepalive', handshake: 'Latest Handshake', handshakeValue: '{seconds: number} seconds ago', }, @@ -278,7 +278,7 @@ If you are an admin/devops - all your customers (instances) and all their tunnel serverPubkey: 'Public Key', endpoint: 'VPN Server Address:Port', dns: 'DNS', - allowedips: 'Allowed IPs (seperate with comma)', + allowedips: 'Allowed IPs (separate with comma)', persistentKeepAlive: 'Persistent Keep Alive (sec)', preUp: 'PreUp', postUp: 'PostUp', @@ -301,7 +301,7 @@ If you are an admin/devops - all your customers (instances) and all their tunnel 'A comma-separated list of IP addresses or CIDR ranges that are allowed for communication through the tunnel.', endpoint: 'The address and port of the WireGuard server, typically in the format "hostname:port".', - dns: 'The DNS (Domain Name System) server that the WireGuard tunnel should use for name resolution.', + dns: 'The DNS (Domain Name System) server that the WireGuard tunnel should use for name resolution. Right now we only support DNS server IP, in the feature we will support domain search.', persistentKeepAlive: 'The interval (in seconds) for sending periodic keep-alive messages to ensure the tunnel stays active. Adjust as needed.', routeAllTraffic: @@ -337,7 +337,7 @@ If you are an admin/devops - all your customers (instances) and all their tunnel
  • Click on the "Import Config File" button.
  • -
  • Navigatge to configuration file using the file selection dialog.
  • +
  • Navigate to configuration file using the file selection dialog.
  • Select the .conf file you received or created.
@@ -408,7 +408,7 @@ If you are an admin/devops - all your customers (instances) and all their tunnel instances: 'defguard Instances', addInstance: 'Add Instance', addTunnel: 'Add Tunnel', - tunnels: 'Wireguard Tunnels', + tunnels: 'WireGuard Tunnels', settings: 'Settings', copyright: { copyright: `Copyright © 2023`, @@ -447,7 +447,7 @@ In order to gain access to the company infrastructure, we require you to complet 1. Verify your data 2. Create your password -3. Configurate VPN device +3. Configure VPN device You have a time limit of **{time: string} minutes** to complete this process. If you have any questions, please consult your assigned admin.All necessary information can be found at the bottom of the sidebar.`, @@ -507,7 +507,7 @@ If you have any questions, please consult your assigned admin.All necessary info create: { submit: 'Create Configuration', messageBox: - 'Please be advised that you have to download the configuration now, since we do not store your private key. After this dialog is closed, you will not be able to get your fulll configuration file (with private keys, only blank template).', + 'Please be advised that you have to download the configuration now, since we do not store your private key. After this dialog is closed, you will not be able to get your full configuration file (with private keys, only blank template).', form: { fields: { name: { @@ -535,7 +535,7 @@ If you have any questions, please consult your assigned admin.All necessary info `, manual: `

- Please be advised that configuration provided here does not include private key and uses public key to fill it's place you will need to repalce it on your own for configuration to work properly. + Please be advised that configuration provided here does not include private key and uses public key to fill it's place you will need to replace it on your own for configuration to work properly.

`, }, @@ -554,7 +554,7 @@ If you have any questions, please consult your assigned admin.All necessary info steps: { wireguard: { content: - 'Download and install WireGuard client on your compputer or app on phone.', + 'Download and install WireGuard client on your computer or app on phone.', button: 'Download WireGuard', }, downloadConfig: 'Download provided configuration file to your device.', @@ -634,7 +634,7 @@ If you want to disengage your VPN connection, simply press "deactivate". messages: { success: '{name: string} updated.', error: 'Token or URL is invalid.', - errorInstanceNotFound: 'Intance for given token is not registered !', + errorInstanceNotFound: 'Instance for given token is not registered !', }, }, deleteInstance: { @@ -642,7 +642,7 @@ If you want to disengage your VPN connection, simply press "deactivate". subtitle: 'Are you sure you want to delete {name: string}?', messages: { success: 'Instance deleted', - error: 'Unexpected error occured', + error: 'Unexpected error occurred', }, controls: { submit: 'Delete instance', @@ -653,7 +653,7 @@ If you want to disengage your VPN connection, simply press "deactivate". subtitle: 'Are you sure you want to delete {name: string}?', messages: { success: 'Tunnel deleted', - error: 'Unexpected error occured', + error: 'Unexpected error occurred', }, controls: { submit: 'Delete tunnel', diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 2d82efcd..921bf368 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -22,7 +22,7 @@ type RootTranslation = { /** * s​e​c​o​n​d​s */ - prular: string + plural: string } minutes: { /** @@ -32,7 +32,7 @@ type RootTranslation = { /** * m​i​n​u​t​e​s */ - prular: string + plural: string } } form: { @@ -163,7 +163,7 @@ type RootTranslation = { */ subtitle: string } - tunel: { + tunnel: { /** * A​d​d​ ​T​u​n​e​l */ @@ -1419,7 +1419,7 @@ export type TranslationFunctions = { /** * seconds */ - prular: () => LocalizedString + plural: () => LocalizedString } minutes: { /** @@ -1429,7 +1429,7 @@ export type TranslationFunctions = { /** * minutes */ - prular: () => LocalizedString + plural: () => LocalizedString } } form: { @@ -1558,9 +1558,9 @@ export type TranslationFunctions = { */ subtitle: () => LocalizedString } - tunel: { + tunnel: { /** - * Add Tunel + * Add Tunnel */ title: () => LocalizedString /** diff --git a/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx index 9478655c..a1fcf63d 100644 --- a/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx +++ b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx @@ -47,8 +47,8 @@ export const WelcomeCardSlide = () => { className="inner-card" onClick={() => navigate(routes.client.addTunnel, { replace: true })} > -

{localLL.tunel.title()}

-

{localLL.tunel.subtitle()}

+

{localLL.tunnel.title()}

+

{localLL.tunnel.subtitle()}

diff --git a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx index 701e3ecb..57bdf9d1 100644 --- a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx +++ b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx @@ -20,7 +20,7 @@ import { useUpdateInstanceModal } from './modals/UpdateInstanceModal/useUpdateIn export const ClientInstancePage = () => { const { LL } = useI18nContext(); const instanceLL = LL.pages.client.pages.instancePage; - const tunelLL = LL.pages.client.pages.tunnelPage; + const tunnelLL = LL.pages.client.pages.tunnelPage; const instances = useClientStore((state) => state.instances); const tunnels = useClientStore((state) => state.tunnels); const [selectedInstanceId, selectedInstanceType] = useClientStore((state) => [ @@ -59,7 +59,7 @@ export const ClientInstancePage = () => { return (
-

{isLocationPage ? instanceLL.title() : tunelLL.title()}

+

{isLocationPage ? instanceLL.title() : tunnelLL.title()}

{isLocationPage && ( diff --git a/src/pages/enrollment/components/TimeLeft/TimeLeft.tsx b/src/pages/enrollment/components/TimeLeft/TimeLeft.tsx index f506af47..a34d6b4e 100644 --- a/src/pages/enrollment/components/TimeLeft/TimeLeft.tsx +++ b/src/pages/enrollment/components/TimeLeft/TimeLeft.tsx @@ -28,7 +28,7 @@ export const TimeLeft = ({ disableLabel }: Props) => { let minutesString = ''; if (minutes > 0) { if (minutes > 1) { - minutesString = `${minutes} ${LL.time.minutes.prular()}`; + minutesString = `${minutes} ${LL.time.minutes.plural()}`; } else { minutesString = `${minutes} ${LL.time.minutes.singular()}`; } @@ -36,7 +36,7 @@ export const TimeLeft = ({ disableLabel }: Props) => { let secondsString = ''; if (seconds > 0) { if (seconds > 1) { - secondsString = `${seconds} ${LL.time.seconds.prular()}`; + secondsString = `${seconds} ${LL.time.seconds.plural()}`; } else { secondsString = `${seconds} ${LL.time.seconds.singular()}`; } diff --git a/src/shared/patterns.ts b/src/shared/patterns.ts index 185f4ec0..6ed22044 100644 --- a/src/shared/patterns.ts +++ b/src/shared/patterns.ts @@ -68,7 +68,7 @@ export const patternValidDomain = /^(?:(?:(?:[a-zA-z\-]+)\:\/{1,3})?(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-\.]){1,61}(?:\.[a-zA-Z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3}))(?:\:[0-9]{1,5})?$/; export const patternValidIp = - /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/32)?$/; export const cidrRegex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}|[0-9a-fA-F:.]+\/\d{1,3})$/; From d42ead129412fcc01a0527acc642ca842c22ccf8 Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Thu, 18 Jan 2024 18:15:20 +0100 Subject: [PATCH 36/45] fix: Add missing links in last carousel page #169 --- src/i18n/en/index.ts | 10 ++-- src/i18n/i18n-types.ts | 50 ++++++++++++++++--- .../CarouselPage/cards/CarouselCards.tsx | 15 ++++++ .../client/pages/CarouselPage/style.scss | 4 ++ src/shared/constants.ts | 7 +++ 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 6aa30f18..168177d0 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -101,10 +101,12 @@ If you are an admin/devops - all your customers (instances) and all their tunnel // md title: '**Support us** on Github', // md - text: `**defguard** is free and truly Open Source and our team has been working on it for several months. Please consider supporting us by: -- staring us on GitHub -- spreading the word about **defguard**! -- join our Matrix server: https://matrix.to/#/#defguard:teonite.com`, + text: `**defguard** is free and truly Open Source and our team has been working on it for several months. Please consider supporting us by: `, + githubText: `staring us on`, + githubLink: `GitHub`, + spreadWordText: `spreading the word about:`, + defguard: `defguard!`, + joinMatrix: `join our Matrix server:`, }, }, }, diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 921bf368..7a2b91f5 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -217,12 +217,29 @@ type RootTranslation = { */ title: string /** - * *​*​d​e​f​g​u​a​r​d​*​*​ ​i​s​ ​f​r​e​e​ ​a​n​d​ ​t​r​u​l​y​ ​O​p​e​n​ ​S​o​u​r​c​e​ ​a​n​d​ ​o​u​r​ ​t​e​a​m​ ​h​a​s​ ​b​e​e​n​ ​w​o​r​k​i​n​g​ ​o​n​ ​i​t​ ​f​o​r​ ​s​e​v​e​r​a​l​ ​m​o​n​t​h​s​.​ ​P​l​e​a​s​e​ ​c​o​n​s​i​d​e​r​ ​s​u​p​p​o​r​t​i​n​g​ ​u​s​ ​b​y​:​ ​ ​ - ​-​ ​s​t​a​r​i​n​g​ ​u​s​ ​o​n​ ​G​i​t​H​u​b​ - ​-​ ​s​p​r​e​a​d​i​n​g​ ​t​h​e​ ​w​o​r​d​ ​a​b​o​u​t​ ​*​*​d​e​f​g​u​a​r​d​*​*​!​ - ​-​ ​j​o​i​n​ ​o​u​r​ ​M​a​t​r​i​x​ ​s​e​r​v​e​r​:​ ​h​t​t​p​s​:​/​/​m​a​t​r​i​x​.​t​o​/​#​/​#​d​e​f​g​u​a​r​d​:​t​e​o​n​i​t​e​.​c​o​m + * *​*​d​e​f​g​u​a​r​d​*​*​ ​i​s​ ​f​r​e​e​ ​a​n​d​ ​t​r​u​l​y​ ​O​p​e​n​ ​S​o​u​r​c​e​ ​a​n​d​ ​o​u​r​ ​t​e​a​m​ ​h​a​s​ ​b​e​e​n​ ​w​o​r​k​i​n​g​ ​o​n​ ​i​t​ ​f​o​r​ ​s​e​v​e​r​a​l​ ​m​o​n​t​h​s​.​ ​P​l​e​a​s​e​ ​c​o​n​s​i​d​e​r​ ​s​u​p​p​o​r​t​i​n​g​ ​u​s​ ​b​y​:​ */ text: string + /** + * s​t​a​r​i​n​g​ ​u​s​ ​o​n + */ + githubText: string + /** + * G​i​t​H​u​b + */ + githubLink: string + /** + * s​p​r​e​a​d​i​n​g​ ​t​h​e​ ​w​o​r​d​ ​a​b​o​u​t​: + */ + spreadWordText: string + /** + * d​e​f​g​u​a​r​d​! + */ + defguard: string + /** + * j​o​i​n​ ​o​u​r​ ​M​a​t​r​i​x​ ​s​e​r​v​e​r​: + */ + joinMatrix: string } } } @@ -1612,12 +1629,29 @@ export type TranslationFunctions = { */ title: () => LocalizedString /** - * **defguard** is free and truly Open Source and our team has been working on it for several months. Please consider supporting us by: - - staring us on GitHub - - spreading the word about **defguard**! - - join our Matrix server: https://matrix.to/#/#defguard:teonite.com + * **defguard** is free and truly Open Source and our team has been working on it for several months. Please consider supporting us by: */ text: () => LocalizedString + /** + * staring us on + */ + githubText: () => LocalizedString + /** + * GitHub + */ + githubLink: () => LocalizedString + /** + * spreading the word about: + */ + spreadWordText: () => LocalizedString + /** + * defguard! + */ + defguard: () => LocalizedString + /** + * join our Matrix server: + */ + joinMatrix: () => LocalizedString } } } diff --git a/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx index a1fcf63d..79d8ec5a 100644 --- a/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx +++ b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx @@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom'; import { useI18nContext } from '../../../../../i18n/i18n-react'; import { IconDefguard } from '../../../../../shared/components/icons/IconDefguard/IconDeguard'; import SvgDefguardLogoText from '../../../../../shared/components/svg/DefguardLogoText'; +import { githubUrl, mastodonUrl, matrixUrl } from '../../../../../shared/constants'; import { Button } from '../../../../../shared/defguard-ui/components/Layout/Button/Button'; import { ButtonSize, @@ -173,6 +174,20 @@ export const SupportSlide = () => {
{localLL.text()} +
diff --git a/src/pages/client/pages/CarouselPage/style.scss b/src/pages/client/pages/CarouselPage/style.scss index e6c81383..bbbb3621 100644 --- a/src/pages/client/pages/CarouselPage/style.scss +++ b/src/pages/client/pages/CarouselPage/style.scss @@ -15,6 +15,10 @@ text-decoration: none; } } + a { + text-decoration: underline; + cursor: pointer; + } .more { @include typography(app-body-1); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 9cd1e6d5..fb750bca 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -5,3 +5,10 @@ export const deviceBreakpoints = { tablet: 768, desktop: 992, }; + +export const mastodonUrl = + 'https://mastodonshare.com/?text=%22Defguard%20is%20awesome!%22&url=defguard.net'; + +export const githubUrl = 'https://github.com/Defguard/defguard'; + +export const matrixUrl = 'https://matrix.to/#/#defguard:teonite.com'; From 40d93f537db89fbab96270290cfc3a8b57fb7817 Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk Date: Fri, 19 Jan 2024 14:18:04 +0100 Subject: [PATCH 37/45] feat: Info card in settings (#170) * feat: Info card in settings --- src/i18n/en/index.ts | 1 + src/i18n/i18n-types.ts | 66 ++++++++------- .../CarouselPage/cards/CarouselCards.tsx | 18 +--- .../ClientSettingsPage/ClientSettingsPage.tsx | 11 ++- .../components/InfoCard/InfoCard.tsx | 83 +++++++++++++++++++ .../pages/ClientSettingsPage/style.scss | 65 ++++++++++++++- src/shared/components/svg/GithubIcon.tsx | 19 +++++ 7 files changed, 213 insertions(+), 50 deletions(-) create mode 100644 src/pages/client/pages/ClientSettingsPage/components/InfoCard/InfoCard.tsx create mode 100644 src/shared/components/svg/GithubIcon.tsx diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 168177d0..092075ff 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -107,6 +107,7 @@ If you are an admin/devops - all your customers (instances) and all their tunnel spreadWordText: `spreading the word about:`, defguard: `defguard!`, joinMatrix: `join our Matrix server:`, + supportUs: 'Support Us!', }, }, }, diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 7a2b91f5..b8c3086d 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -165,7 +165,7 @@ type RootTranslation = { } tunnel: { /** - * A​d​d​ ​T​u​n​e​l + * A​d​d​ ​T​u​n​n​e​l */ title: string /** @@ -180,9 +180,9 @@ type RootTranslation = { */ title: string /** - * S​i​n​c​e​ ​W​i​r​e​g​u​a​r​d​ ​p​r​o​t​o​c​o​l​ ​d​o​e​s​n​'​t​ ​s​u​p​p​o​r​t​ ​2​F​A​/​M​F​A​ ​-​ ​m​o​s​t​ ​(​i​f​ ​n​o​t​ ​a​l​l​)​ ​c​u​r​r​e​n​t​l​y​ ​a​v​a​i​l​a​b​l​e​ ​W​i​r​e​g​u​a​r​d​ ​c​l​i​e​n​t​s​ ​d​o​ ​n​o​t​ ​s​u​p​p​o​r​t​ ​r​e​a​l​ ​M​u​l​t​i​-​F​a​c​t​o​r​ ​A​u​t​h​e​n​t​i​c​a​t​i​o​n​/​2​F​A​ ​-​ ​a​n​d​ ​u​s​e​ ​2​F​A​ ​j​u​s​t​ ​a​s​ ​a​u​t​h​o​r​i​z​a​t​i​o​n​ ​t​o​ ​t​h​e​ ​"​a​p​p​l​i​c​a​t​i​o​n​"​ ​i​t​s​e​l​f​ ​(​a​n​d​ ​n​o​t​ ​W​i​r​e​g​u​a​r​d​ ​t​u​n​n​e​l​)​.​ ​ ​ + * S​i​n​c​e​ ​W​i​r​e​G​u​a​r​d​ ​p​r​o​t​o​c​o​l​ ​d​o​e​s​n​'​t​ ​s​u​p​p​o​r​t​ ​2​F​A​/​M​F​A​ ​-​ ​m​o​s​t​ ​(​i​f​ ​n​o​t​ ​a​l​l​)​ ​c​u​r​r​e​n​t​l​y​ ​a​v​a​i​l​a​b​l​e​ ​W​i​r​e​G​u​a​r​d​ ​c​l​i​e​n​t​s​ ​d​o​ ​n​o​t​ ​s​u​p​p​o​r​t​ ​r​e​a​l​ ​M​u​l​t​i​-​F​a​c​t​o​r​ ​A​u​t​h​e​n​t​i​c​a​t​i​o​n​/​2​F​A​ ​-​ ​a​n​d​ ​u​s​e​ ​2​F​A​ ​j​u​s​t​ ​a​s​ ​a​u​t​h​o​r​i​z​a​t​i​o​n​ ​t​o​ ​t​h​e​ ​"​a​p​p​l​i​c​a​t​i​o​n​"​ ​i​t​s​e​l​f​ ​(​a​n​d​ ​n​o​t​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​)​.​ ​ ​ ​ - ​I​f​ ​y​o​u​ ​w​o​u​l​d​ ​l​i​k​e​ ​t​o​ ​s​e​c​u​r​e​ ​y​o​u​r​ ​W​i​r​e​g​u​a​r​d​ ​i​n​s​t​a​n​c​e​ ​t​r​y​ ​*​*​d​e​f​g​u​a​r​d​*​*​ ​V​P​N​ ​&​ ​S​S​O​ ​s​e​r​v​e​r​ ​(​w​h​i​c​h​ ​i​s​ ​a​l​s​o​ ​f​r​e​e​ ​&​ ​o​p​e​n​ ​s​o​u​r​c​e​)​ ​t​o​ ​g​e​t​ ​r​e​a​l​ ​2​F​A​ ​u​s​i​n​g​ ​W​i​r​e​g​u​a​r​d​ ​P​S​K​ ​k​e​y​s​ ​a​n​d​ ​p​e​e​r​s​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​b​y​ ​d​e​f​g​u​a​r​d​ ​g​a​t​e​w​a​y​! + ​I​f​ ​y​o​u​ ​w​o​u​l​d​ ​l​i​k​e​ ​t​o​ ​s​e​c​u​r​e​ ​y​o​u​r​ ​W​i​r​e​G​u​a​r​d​ ​i​n​s​t​a​n​c​e​ ​t​r​y​ ​*​*​d​e​f​g​u​a​r​d​*​*​ ​V​P​N​ ​&​ ​S​S​O​ ​s​e​r​v​e​r​ ​(​w​h​i​c​h​ ​i​s​ ​a​l​s​o​ ​f​r​e​e​ ​&​ ​o​p​e​n​ ​s​o​u​r​c​e​)​ ​t​o​ ​g​e​t​ ​r​e​a​l​ ​2​F​A​ ​u​s​i​n​g​ ​W​i​r​e​G​u​a​r​d​ ​P​S​K​ ​k​e​y​s​ ​a​n​d​ ​p​e​e​r​s​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​b​y​ ​d​e​f​g​u​a​r​d​ ​g​a​t​e​w​a​y​! */ sideText: string } @@ -240,6 +240,10 @@ type RootTranslation = { * j​o​i​n​ ​o​u​r​ ​M​a​t​r​i​x​ ​s​e​r​v​e​r​: */ joinMatrix: string + /** + * S​u​p​p​o​r​t​ ​U​s​! + */ + supportUs: string } } } @@ -551,7 +555,7 @@ type RootTranslation = { */ dns: string /** - * P​e​r​s​i​s​t​e​n​ ​k​e​e​p​a​l​i​v​e + * P​e​r​s​i​s​t​e​n​t​ ​k​e​e​p​a​l​i​v​e */ keepalive: string /** @@ -653,7 +657,7 @@ type RootTranslation = { */ dns: string /** - * A​l​l​o​w​e​d​ ​I​P​s​ ​(​s​e​p​e​r​a​t​e​ ​w​i​t​h​ ​c​o​m​m​a​) + * A​l​l​o​w​e​d​ ​I​P​s​ ​(​s​e​p​a​r​a​t​e​ ​w​i​t​h​ ​c​o​m​m​a​) */ allowedips: string /** @@ -711,7 +715,7 @@ type RootTranslation = { */ endpoint: string /** - * T​h​e​ ​D​N​S​ ​(​D​o​m​a​i​n​ ​N​a​m​e​ ​S​y​s​t​e​m​)​ ​s​e​r​v​e​r​ ​t​h​a​t​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​ ​s​h​o​u​l​d​ ​u​s​e​ ​f​o​r​ ​n​a​m​e​ ​r​e​s​o​l​u​t​i​o​n​. + * T​h​e​ ​D​N​S​ ​(​D​o​m​a​i​n​ ​N​a​m​e​ ​S​y​s​t​e​m​)​ ​s​e​r​v​e​r​ ​t​h​a​t​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​ ​s​h​o​u​l​d​ ​u​s​e​ ​f​o​r​ ​n​a​m​e​ ​r​e​s​o​l​u​t​i​o​n​.​ ​R​i​g​h​t​ ​n​o​w​ ​w​e​ ​o​n​l​y​ ​s​u​p​p​o​r​t​ ​D​N​S​ ​s​e​r​v​e​r​ ​I​P​,​ ​i​n​ ​t​h​e​ ​f​e​a​t​u​r​e​ ​w​e​ ​w​i​l​l​ ​s​u​p​p​o​r​t​ ​d​o​m​a​i​n​ ​s​e​a​r​c​h​. */ dns: string /** @@ -789,7 +793,7 @@ type RootTranslation = { ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​d​i​v​>​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​u​l​>​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​l​i​>​ ​C​l​i​c​k​ ​o​n​ ​t​h​e​ ​"​I​m​p​o​r​t​ ​C​o​n​f​i​g​ ​F​i​l​e​"​ ​b​u​t​t​o​n​.​<​/​l​i​>​ - ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​l​i​>​ ​N​a​v​i​g​a​t​g​e​ ​t​o​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​f​i​l​e​ ​u​s​i​n​g​ ​t​h​e​ ​f​i​l​e​ ​s​e​l​e​c​t​i​o​n​ ​d​i​a​l​o​g​.​<​/​l​i​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​l​i​>​ ​N​a​v​i​g​a​t​e​ ​t​o​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​f​i​l​e​ ​u​s​i​n​g​ ​t​h​e​ ​f​i​l​e​ ​s​e​l​e​c​t​i​o​n​ ​d​i​a​l​o​g​.​<​/​l​i​>​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​l​i​>​ ​S​e​l​e​c​t​ ​t​h​e​ ​.​c​o​n​f​ ​f​i​l​e​ ​y​o​u​ ​r​e​c​e​i​v​e​d​ ​o​r​ ​c​r​e​a​t​e​d​.​<​/​l​i​>​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​/​u​l​>​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​/​d​i​v​>​ @@ -910,7 +914,7 @@ type RootTranslation = { */ addTunnel: string /** - * W​i​r​e​g​u​a​r​d​ ​T​u​n​n​e​l​s + * W​i​r​e​G​u​a​r​d​ ​T​u​n​n​e​l​s */ tunnels: string /** @@ -1008,7 +1012,7 @@ type RootTranslation = { ​ ​1​.​ ​V​e​r​i​f​y​ ​y​o​u​r​ ​d​a​t​a​ ​2​.​ ​C​r​e​a​t​e​ ​y​o​u​r​ ​p​a​s​s​w​o​r​d​ - ​3​.​ ​C​o​n​f​i​g​u​r​a​t​e​ ​V​P​N​ ​d​e​v​i​c​e​ + ​3​.​ ​C​o​n​f​i​g​u​r​e​ ​V​P​N​ ​d​e​v​i​c​e​ ​ ​Y​o​u​ ​h​a​v​e​ ​a​ ​t​i​m​e​ ​l​i​m​i​t​ ​o​f​ ​*​*​{​t​i​m​e​}​ ​m​i​n​u​t​e​s​*​*​ ​t​o​ ​c​o​m​p​l​e​t​e​ ​t​h​i​s​ ​p​r​o​c​e​s​s​.​ ​I​f​ ​y​o​u​ ​h​a​v​e​ ​a​n​y​ ​q​u​e​s​t​i​o​n​s​,​ ​p​l​e​a​s​e​ ​c​o​n​s​u​l​t​ ​y​o​u​r​ ​a​s​s​i​g​n​e​d​ ​a​d​m​i​n​.​A​l​l​ ​n​e​c​e​s​s​a​r​y​ ​i​n​f​o​r​m​a​t​i​o​n​ ​c​a​n​ ​b​e​ ​f​o​u​n​d​ ​a​t​ ​t​h​e​ ​b​o​t​t​o​m​ ​o​f​ ​t​h​e​ ​s​i​d​e​b​a​r​. @@ -1121,7 +1125,7 @@ type RootTranslation = { */ submit: string /** - * P​l​e​a​s​e​ ​b​e​ ​a​d​v​i​s​e​d​ ​t​h​a​t​ ​y​o​u​ ​h​a​v​e​ ​t​o​ ​d​o​w​n​l​o​a​d​ ​t​h​e​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​n​o​w​,​ ​s​i​n​c​e​ ​w​e​ ​d​o​ ​n​o​t​ ​s​t​o​r​e​ ​y​o​u​r​ ​p​r​i​v​a​t​e​ ​k​e​y​.​ ​A​f​t​e​r​ ​t​h​i​s​ ​d​i​a​l​o​g​ ​i​s​ ​c​l​o​s​e​d​,​ ​y​o​u​ ​w​i​l​l​ ​n​o​t​ ​b​e​ ​a​b​l​e​ ​t​o​ ​g​e​t​ ​y​o​u​r​ ​f​u​l​l​l​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​f​i​l​e​ ​(​w​i​t​h​ ​p​r​i​v​a​t​e​ ​k​e​y​s​,​ ​o​n​l​y​ ​b​l​a​n​k​ ​t​e​m​p​l​a​t​e​)​. + * P​l​e​a​s​e​ ​b​e​ ​a​d​v​i​s​e​d​ ​t​h​a​t​ ​y​o​u​ ​h​a​v​e​ ​t​o​ ​d​o​w​n​l​o​a​d​ ​t​h​e​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​n​o​w​,​ ​s​i​n​c​e​ ​w​e​ ​d​o​ ​n​o​t​ ​s​t​o​r​e​ ​y​o​u​r​ ​p​r​i​v​a​t​e​ ​k​e​y​.​ ​A​f​t​e​r​ ​t​h​i​s​ ​d​i​a​l​o​g​ ​i​s​ ​c​l​o​s​e​d​,​ ​y​o​u​ ​w​i​l​l​ ​n​o​t​ ​b​e​ ​a​b​l​e​ ​t​o​ ​g​e​t​ ​y​o​u​r​ ​f​u​l​l​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​f​i​l​e​ ​(​w​i​t​h​ ​p​r​i​v​a​t​e​ ​k​e​y​s​,​ ​o​n​l​y​ ​b​l​a​n​k​ ​t​e​m​p​l​a​t​e​)​. */ messageBox: string form: { @@ -1167,7 +1171,7 @@ type RootTranslation = { /** * ​ ​ ​ ​ ​ ​ ​ ​ ​<​p​>​ - ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​P​l​e​a​s​e​ ​b​e​ ​a​d​v​i​s​e​d​ ​t​h​a​t​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​p​r​o​v​i​d​e​d​ ​h​e​r​e​ ​<​s​t​r​o​n​g​>​ ​d​o​e​s​ ​n​o​t​ ​i​n​c​l​u​d​e​ ​p​r​i​v​a​t​e​ ​k​e​y​ ​a​n​d​ ​u​s​e​s​ ​p​u​b​l​i​c​ ​k​e​y​ ​t​o​ ​f​i​l​l​ ​i​t​'​s​ ​p​l​a​c​e​ ​<​/​s​t​r​o​n​g​>​ ​y​o​u​ ​w​i​l​l​ ​n​e​e​d​ ​t​o​ ​r​e​p​a​l​c​e​ ​i​t​ ​o​n​ ​y​o​u​r​ ​o​w​n​ ​f​o​r​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​t​o​ ​w​o​r​k​ ​p​r​o​p​e​r​l​y​.​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​P​l​e​a​s​e​ ​b​e​ ​a​d​v​i​s​e​d​ ​t​h​a​t​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​p​r​o​v​i​d​e​d​ ​h​e​r​e​ ​<​s​t​r​o​n​g​>​ ​d​o​e​s​ ​n​o​t​ ​i​n​c​l​u​d​e​ ​p​r​i​v​a​t​e​ ​k​e​y​ ​a​n​d​ ​u​s​e​s​ ​p​u​b​l​i​c​ ​k​e​y​ ​t​o​ ​f​i​l​l​ ​i​t​'​s​ ​p​l​a​c​e​ ​<​/​s​t​r​o​n​g​>​ ​y​o​u​ ​w​i​l​l​ ​n​e​e​d​ ​t​o​ ​r​e​p​l​a​c​e​ ​i​t​ ​o​n​ ​y​o​u​r​ ​o​w​n​ ​f​o​r​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​t​o​ ​w​o​r​k​ ​p​r​o​p​e​r​l​y​.​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​/​p​>​ */ @@ -1206,7 +1210,7 @@ type RootTranslation = { steps: { wireguard: { /** - * D​o​w​n​l​o​a​d​ ​a​n​d​ ​i​n​s​t​a​l​l​ ​W​i​r​e​G​u​a​r​d​ ​c​l​i​e​n​t​ ​o​n​ ​y​o​u​r​ ​c​o​m​p​p​u​t​e​r​ ​o​r​ ​a​p​p​ ​o​n​ ​p​h​o​n​e​. + * D​o​w​n​l​o​a​d​ ​a​n​d​ ​i​n​s​t​a​l​l​ ​W​i​r​e​G​u​a​r​d​ ​c​l​i​e​n​t​ ​o​n​ ​y​o​u​r​ ​c​o​m​p​u​t​e​r​ ​o​r​ ​a​p​p​ ​o​n​ ​p​h​o​n​e​. */ content: string /** @@ -1364,7 +1368,7 @@ type RootTranslation = { */ error: string /** - * I​n​t​a​n​c​e​ ​f​o​r​ ​g​i​v​e​n​ ​t​o​k​e​n​ ​i​s​ ​n​o​t​ ​r​e​g​i​s​t​e​r​e​d​ ​! + * I​n​s​t​a​n​c​e​ ​f​o​r​ ​g​i​v​e​n​ ​t​o​k​e​n​ ​i​s​ ​n​o​t​ ​r​e​g​i​s​t​e​r​e​d​ ​! */ errorInstanceNotFound: string } @@ -1385,7 +1389,7 @@ type RootTranslation = { */ success: string /** - * U​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​e​d + * U​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​r​e​d */ error: string } @@ -1412,7 +1416,7 @@ type RootTranslation = { */ success: string /** - * U​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​e​d + * U​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​r​e​d */ error: string } @@ -1592,9 +1596,9 @@ export type TranslationFunctions = { */ title: () => LocalizedString /** - * Since Wireguard protocol doesn't support 2FA/MFA - most (if not all) currently available Wireguard clients do not support real Multi-Factor Authentication/2FA - and use 2FA just as authorization to the "application" itself (and not Wireguard tunnel). + * Since WireGuard protocol doesn't support 2FA/MFA - most (if not all) currently available WireGuard clients do not support real Multi-Factor Authentication/2FA - and use 2FA just as authorization to the "application" itself (and not WireGuard tunnel). - If you would like to secure your Wireguard instance try **defguard** VPN & SSO server (which is also free & open source) to get real 2FA using Wireguard PSK keys and peers configuration by defguard gateway! + If you would like to secure your WireGuard instance try **defguard** VPN & SSO server (which is also free & open source) to get real 2FA using WireGuard PSK keys and peers configuration by defguard gateway! */ sideText: () => LocalizedString } @@ -1652,6 +1656,10 @@ export type TranslationFunctions = { * join our Matrix server: */ joinMatrix: () => LocalizedString + /** + * Support Us! + */ + supportUs: () => LocalizedString } } } @@ -1963,7 +1971,7 @@ export type TranslationFunctions = { */ dns: () => LocalizedString /** - * Persisten keepalive + * Persistent keepalive */ keepalive: () => LocalizedString /** @@ -2064,7 +2072,7 @@ export type TranslationFunctions = { */ dns: () => LocalizedString /** - * Allowed IPs (seperate with comma) + * Allowed IPs (separate with comma) */ allowedips: () => LocalizedString /** @@ -2122,7 +2130,7 @@ export type TranslationFunctions = { */ endpoint: () => LocalizedString /** - * The DNS (Domain Name System) server that the WireGuard tunnel should use for name resolution. + * The DNS (Domain Name System) server that the WireGuard tunnel should use for name resolution. Right now we only support DNS server IP, in the feature we will support domain search. */ dns: () => LocalizedString /** @@ -2200,7 +2208,7 @@ export type TranslationFunctions = {
  • Click on the "Import Config File" button.
  • -
  • Navigatge to configuration file using the file selection dialog.
  • +
  • Navigate to configuration file using the file selection dialog.
  • Select the .conf file you received or created.
@@ -2321,7 +2329,7 @@ export type TranslationFunctions = { */ addTunnel: () => LocalizedString /** - * Wireguard Tunnels + * WireGuard Tunnels */ tunnels: () => LocalizedString /** @@ -2417,7 +2425,7 @@ export type TranslationFunctions = { 1. Verify your data 2. Create your password - 3. Configurate VPN device + 3. Configure VPN device You have a time limit of **{time} minutes** to complete this process. If you have any questions, please consult your assigned admin.All necessary information can be found at the bottom of the sidebar. @@ -2529,7 +2537,7 @@ export type TranslationFunctions = { */ submit: () => LocalizedString /** - * Please be advised that you have to download the configuration now, since we do not store your private key. After this dialog is closed, you will not be able to get your fulll configuration file (with private keys, only blank template). + * Please be advised that you have to download the configuration now, since we do not store your private key. After this dialog is closed, you will not be able to get your full configuration file (with private keys, only blank template). */ messageBox: () => LocalizedString form: { @@ -2575,7 +2583,7 @@ export type TranslationFunctions = { /** *

- Please be advised that configuration provided here does not include private key and uses public key to fill it's place you will need to repalce it on your own for configuration to work properly. + Please be advised that configuration provided here does not include private key and uses public key to fill it's place you will need to replace it on your own for configuration to work properly.

*/ @@ -2613,7 +2621,7 @@ export type TranslationFunctions = { steps: { wireguard: { /** - * Download and install WireGuard client on your compputer or app on phone. + * Download and install WireGuard client on your computer or app on phone. */ content: () => LocalizedString /** @@ -2770,7 +2778,7 @@ export type TranslationFunctions = { */ error: () => LocalizedString /** - * Intance for given token is not registered ! + * Instance for given token is not registered ! */ errorInstanceNotFound: () => LocalizedString } @@ -2790,7 +2798,7 @@ export type TranslationFunctions = { */ success: () => LocalizedString /** - * Unexpected error occured + * Unexpected error occurred */ error: () => LocalizedString } @@ -2816,7 +2824,7 @@ export type TranslationFunctions = { */ success: () => LocalizedString /** - * Unexpected error occured + * Unexpected error occurred */ error: () => LocalizedString } diff --git a/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx index 79d8ec5a..65635af4 100644 --- a/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx +++ b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx @@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom'; import { useI18nContext } from '../../../../../i18n/i18n-react'; import { IconDefguard } from '../../../../../shared/components/icons/IconDefguard/IconDeguard'; import SvgDefguardLogoText from '../../../../../shared/components/svg/DefguardLogoText'; +import { GitHubIcon } from '../../../../../shared/components/svg/GithubIcon'; import { githubUrl, mastodonUrl, matrixUrl } from '../../../../../shared/constants'; import { Button } from '../../../../../shared/defguard-ui/components/Layout/Button/Button'; import { @@ -76,21 +77,6 @@ export const TwoFaSlide = () => { ); }; -const GithubIcon = () => { - return ( - - - - - - - ); -}; - const GithubButton = () => { const { LL } = useI18nContext(); const localLL = LL.pages.client.pages.carouselPage.slides.shared; @@ -100,7 +86,7 @@ const GithubButton = () => { size={ButtonSize.LARGE} styleVariant={ButtonStyleVariant.PRIMARY} text={localLL.githubButton()} - rightIcon={} + rightIcon={} onClick={() => { openLink(defguardGithubLink); }} diff --git a/src/pages/client/pages/ClientSettingsPage/ClientSettingsPage.tsx b/src/pages/client/pages/ClientSettingsPage/ClientSettingsPage.tsx index 2f5f0575..0ed84e87 100644 --- a/src/pages/client/pages/ClientSettingsPage/ClientSettingsPage.tsx +++ b/src/pages/client/pages/ClientSettingsPage/ClientSettingsPage.tsx @@ -3,18 +3,23 @@ import './style.scss'; import { useI18nContext } from '../../../../i18n/i18n-react'; import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card'; import { GlobalSettingsTab } from './components/GlobalSettingsTab/GlobalSettingsTab'; +import { InfoCard } from './components/InfoCard/InfoCard'; export const ClientSettingsPage = () => { const { LL } = useI18nContext(); const pageLL = LL.pages.client.pages.settingsPage; + return (

{pageLL.title()}

- - - +
+ + + + +
); }; diff --git a/src/pages/client/pages/ClientSettingsPage/components/InfoCard/InfoCard.tsx b/src/pages/client/pages/ClientSettingsPage/components/InfoCard/InfoCard.tsx new file mode 100644 index 00000000..ed7bca1a --- /dev/null +++ b/src/pages/client/pages/ClientSettingsPage/components/InfoCard/InfoCard.tsx @@ -0,0 +1,83 @@ +import Markdown from 'react-markdown'; + +import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { GitHubIcon } from '../../../../../../shared/components/svg/GithubIcon'; +import { githubUrl, mastodonUrl, matrixUrl } from '../../../../../../shared/constants'; +import { Button } from '../../../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { + ButtonSize, + ButtonStyleVariant, +} from '../../../../../../shared/defguard-ui/components/Layout/Button/types'; +import { Card } from '../../../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { defguardGithubLink } from '../../../../../../shared/links'; +import { clientApi } from '../../../../clientAPI/clientApi'; +import securityImage from '../../../CarouselPage/cards/assets/slide_security.png'; + +const { openLink } = clientApi; + +const GithubButton = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.carouselPage.slides.shared; + return ( +