diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 22cc9d03..7db2966c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -273,3 +273,77 @@ jobs: asset_path: src-tauri/target/${{ matrix.target }}/product-signed/defguard.pkg asset_name: defguard-${{ matrix.target }}-${{ env.VERSION }}.pkg asset_content_type: application/octet-stream + + build-windows: + needs: + - create-release + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: "recursive" + + - name: Write release version + run: | + $env:VERSION=echo ($env:GITHUB_REF_NAME.Substring(1) -Split "-")[0] + echo Version: $env:VERSION + echo "VERSION=$env:VERSION" >> $env:GITHUB_ENV + + - uses: actions/setup-node@v3 + with: + node-version: "20" + + - uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $env:GITHUB_ENV + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-build-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-build-store- + + - name: Install deps + run: pnpm install --frozen-lockfile + - uses: dtolnay/rust-toolchain@stable + + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Remove "default-run" line from Cargo.toml + run: | + Set-Content -Path ".\src-tauri\Cargo.toml" -Value (get-content -Path ".\src-tauri\Cargo.toml" | Select-String -Pattern 'default-run =' -NotMatch) + + - name: Build packages + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Bundle application + run: | + dotnet tool install --global wix + wix --version + wix extension add -g WixToolset.Bal.wixext + wix build .\src-tauri\resources-windows\defguard-client.wxs -ext WixToolset.Bal.wixext + + - name: Upload installer + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: src-tauri/resources-windows/defguard-client.exe + asset_name: defguard-client_${{ env.VERSION }}_x64_en-US.exe + asset_content_type: application/octet-stream + \ No newline at end of file diff --git a/README.md b/README.md index 1bedcb4d..9858bae5 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,7 @@ pnpm install ```bash pnpm tauri dev ``` + +### Windows + +Remove `default-run` line from `[package]` section in `Cargo.toml` to build the project. diff --git a/package.json b/package.json index 2074284d..7a63441b 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", @@ -51,6 +51,7 @@ "@types/byte-size": "^8.1.2", "byte-size": "^8.1.1", "classnames": "^2.3.2", + "compare-versions": "^6.1.0", "dayjs": "^1.11.10", "detect-browser": "^5.3.0", "fast-deep-equal": "^3.1.3", @@ -63,6 +64,7 @@ "prop-types": "^15.8.1", "radash": "^11.0.0", "react": "^18.2.0", + "react-auth-code-input": "^3.2.1", "react-click-away-listener": "^2.2.3", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9589e21b..91835893 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ dependencies: classnames: specifier: ^2.3.2 version: 2.3.2 + compare-versions: + specifier: ^6.1.0 + version: 6.1.0 dayjs: specifier: ^1.11.10 version: 1.11.10 @@ -74,6 +77,9 @@ dependencies: react: specifier: ^18.2.0 version: 18.2.0 + react-auth-code-input: + specifier: ^3.2.1 + version: 3.2.1(react@18.2.0) react-click-away-listener: specifier: ^2.2.3 version: 2.2.3(react-dom@18.2.0)(react@18.2.0) @@ -2385,6 +2391,10 @@ packages: engines: {node: ^12.20.0 || >=14} dev: true + /compare-versions@6.1.0: + resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -5829,6 +5839,15 @@ packages: engines: {node: '>=14.18.0'} dev: false + /react-auth-code-input@3.2.1(react@18.2.0): + resolution: {integrity: sha512-oWWKbxDLU5g46gvE1DIvNFHsDx37JSAfB6WX8luG6TWZB3iDfKMjQnhUgb+0imL/6ykGVMqdZ426tQW1uj25kg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16.0.0' + dependencies: + react: 18.2.0 + dev: false + /react-click-away-listener@2.2.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-p63JRQtK9d085+QHUJ2Pje22P/N4tEaXsS2x7tbbptriQqZ9o8xEk7G1JrxwND5YmEVc/VO4fC3+cSBsqqgLUQ==} peerDependencies: diff --git a/src-tauri/.sqlx/query-15653b6cff48d598efc8bf6b40ea414580a236b351ad7818ee75d7efe2833882.json b/src-tauri/.sqlx/query-15653b6cff48d598efc8bf6b40ea414580a236b351ad7818ee75d7efe2833882.json new file mode 100644 index 00000000..79bd08c4 --- /dev/null +++ b/src-tauri/.sqlx/query-15653b6cff48d598efc8bf6b40ea414580a236b351ad7818ee75d7efe2833882.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO tunnel_stats (tunnel_id, upload, download, last_handshake, collected_at, listen_port, persistent_keepalive_interval) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id;", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 7 + }, + "nullable": [ + false + ] + }, + "hash": "15653b6cff48d598efc8bf6b40ea414580a236b351ad7818ee75d7efe2833882" +} 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-1c996712f62a1005990733cd9eee7a94bdcf2ef01b559304aea1d642fab7ae22.json b/src-tauri/.sqlx/query-1c996712f62a1005990733cd9eee7a94bdcf2ef01b559304aea1d642fab7ae22.json new file mode 100644 index 00000000..0da21a4f --- /dev/null +++ b/src-tauri/.sqlx/query-1c996712f62a1005990733cd9eee7a94bdcf2ef01b559304aea1d642fab7ae22.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM location WHERE id = $1;", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "1c996712f62a1005990733cd9eee7a94bdcf2ef01b559304aea1d642fab7ae22" +} 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-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-8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f.json b/src-tauri/.sqlx/query-368814ed137d486c51e412c386ccd82ebaa96f1d438388521b4761da5bb93ce4.json similarity index 74% rename from src-tauri/.sqlx/query-8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f.json rename to src-tauri/.sqlx/query-368814ed137d486c51e412c386ccd82ebaa96f1d438388521b4761da5bb93ce4.json index d628e720..425a1789 100644 --- a/src-tauri/.sqlx/query-8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f.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 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": "8b86e04ec40a25f43d431ee84a7cc3131c526a683be8d6739fedd40a9d56c41f" + "hash": "368814ed137d486c51e412c386ccd82ebaa96f1d438388521b4761da5bb93ce4" } diff --git a/src-tauri/.sqlx/query-3ebe4e16b18856b635cfe1b9b8d76e1843187308ebf48578019de523494cd4fc.json b/src-tauri/.sqlx/query-3ebe4e16b18856b635cfe1b9b8d76e1843187308ebf48578019de523494cd4fc.json new file mode 100644 index 00000000..96a0b3a6 --- /dev/null +++ b/src-tauri/.sqlx/query-3ebe4e16b18856b635cfe1b9b8d76e1843187308ebf48578019de523494cd4fc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM instance WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "3ebe4e16b18856b635cfe1b9b8d76e1843187308ebf48578019de523494cd4fc" +} 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-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-44a0d1740d6a566f55faf25f4839aaf3d6dd41baa587381c50b0387e1a707938.json b/src-tauri/.sqlx/query-44a0d1740d6a566f55faf25f4839aaf3d6dd41baa587381c50b0387e1a707938.json new file mode 100644 index 00000000..1e9d13e8 --- /dev/null +++ b/src-tauri/.sqlx/query-44a0d1740d6a566f55faf25f4839aaf3d6dd41baa587381c50b0387e1a707938.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO settings (log_level, theme, tray_icon_theme, check_for_updates) VALUES ($1, $2, $3, $4);", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "44a0d1740d6a566f55faf25f4839aaf3d6dd41baa587381c50b0387e1a707938" +} diff --git a/src-tauri/.sqlx/query-45dee5e00c040e079779b1202cd1fafaff49e75319a3533279ec6ff574fd77cc.json b/src-tauri/.sqlx/query-45dee5e00c040e079779b1202cd1fafaff49e75319a3533279ec6ff574fd77cc.json new file mode 100644 index 00000000..d5183b62 --- /dev/null +++ b/src-tauri/.sqlx/query-45dee5e00c040e079779b1202cd1fafaff49e75319a3533279ec6ff574fd77cc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE tunnel SET name = $1, pubkey = $2, prvkey = $3, address = $4, server_pubkey = $5, allowed_ips = $6, endpoint = $7, dns = $8, persistent_keep_alive = $9, route_all_traffic = $10, pre_up = $11, post_up = $12, pre_down = $13, post_down = $14 WHERE id = $15;", + "describe": { + "columns": [], + "parameters": { + "Right": 15 + }, + "nullable": [] + }, + "hash": "45dee5e00c040e079779b1202cd1fafaff49e75319a3533279ec6ff574fd77cc" +} 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/.sqlx/query-5c28754680e92a81a74ced06551494c2471bf7180483796d1ff4419a5e9b488d.json b/src-tauri/.sqlx/query-5c28754680e92a81a74ced06551494c2471bf7180483796d1ff4419a5e9b488d.json new file mode 100644 index 00000000..873b1935 --- /dev/null +++ b/src-tauri/.sqlx/query-5c28754680e92a81a74ced06551494c2471bf7180483796d1ff4419a5e9b488d.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n WITH cte AS (\n SELECT\n id, tunnel_id,\n COALESCE(upload - LAG(upload) OVER (PARTITION BY tunnel_id ORDER BY collected_at), 0) as upload,\n COALESCE(download - LAG(download) OVER (PARTITION BY tunnel_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 tunnel_stats\n ORDER BY collected_at\n LIMIT -1 OFFSET 1\n )\n SELECT\n id, tunnel_id,\n SUM(MAX(upload, 0)) as \"upload!: i64\",\n SUM(MAX(download, 0)) as \"download!: i64\",\n last_handshake,\n collected_at as \"collected_at!: NaiveDateTime\",\n listen_port as \"listen_port!: u32\",\n persistent_keepalive_interval as \"persistent_keepalive_interval?: u16\"\n FROM cte\n WHERE tunnel_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": "tunnel_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, + false + ] + }, + "hash": "5c28754680e92a81a74ced06551494c2471bf7180483796d1ff4419a5e9b488d" +} diff --git a/src-tauri/.sqlx/query-66208ae39fb096ab67d767447c7671429006b303eb6aeb452b1a99716a933ba6.json b/src-tauri/.sqlx/query-66208ae39fb096ab67d767447c7671429006b303eb6aeb452b1a99716a933ba6.json new file mode 100644 index 00000000..75f9cb7e --- /dev/null +++ b/src-tauri/.sqlx/query-66208ae39fb096ab67d767447c7671429006b303eb6aeb452b1a99716a933ba6.json @@ -0,0 +1,104 @@ +{ + "db_name": "SQLite", + "query": "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;", + "describe": { + "columns": [ + { + "name": "id?", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "pubkey", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "prvkey", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "address", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "server_pubkey", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "allowed_ips", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "endpoint", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "dns", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "persistent_keep_alive", + "ordinal": 9, + "type_info": "Int64" + }, + { + "name": "route_all_traffic", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "pre_up", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "post_up", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "pre_down", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "post_down", + "ordinal": 14, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + true, + true, + true, + true + ] + }, + "hash": "66208ae39fb096ab67d767447c7671429006b303eb6aeb452b1a99716a933ba6" +} 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-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-8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd.json b/src-tauri/.sqlx/query-8dfaa1c23af01a966442b52412eaa3a58f804f5d59d14fa70a65dba68c079b01.json similarity index 74% rename from src-tauri/.sqlx/query-8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd.json rename to src-tauri/.sqlx/query-8dfaa1c23af01a966442b52412eaa3a58f804f5d59d14fa70a65dba68c079b01.json index fc136275..beaa490d 100644 --- a/src-tauri/.sqlx/query-8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd.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 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 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": "8159b0d79bc84fcbb69059973d3150f8e55473aa836723604812927003fc50bd" + "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-c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78.json b/src-tauri/.sqlx/query-c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78.json new file mode 100644 index 00000000..5d303f03 --- /dev/null +++ b/src-tauri/.sqlx/query-c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78.json @@ -0,0 +1,104 @@ +{ + "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 server_pubkey = $1;", + "describe": { + "columns": [ + { + "name": "id?", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "pubkey", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "prvkey", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "address", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "server_pubkey", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "allowed_ips", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "endpoint", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "dns", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "persistent_keep_alive", + "ordinal": 9, + "type_info": "Int64" + }, + { + "name": "route_all_traffic", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "pre_up", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "post_up", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "pre_down", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "post_down", + "ordinal": 14, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + true, + true, + true, + true + ] + }, + "hash": "c372f0b7ed83311ea369a309b6da796e6944d87b1be160ce7aa2cdbf57c20e78" +} 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/.sqlx/query-8c85b8c4cfd2b85ff96db3ce58ce727b3c77039d5984f523bb1923c8b81cbcd1.json b/src-tauri/.sqlx/query-ccb326ab9b3ebd336c0fd9aed0da8e991f091f17e25583bd251347c207d8ff5c.json similarity index 74% rename from src-tauri/.sqlx/query-8c85b8c4cfd2b85ff96db3ce58ce727b3c77039d5984f523bb1923c8b81cbcd1.json rename to src-tauri/.sqlx/query-ccb326ab9b3ebd336c0fd9aed0da8e991f091f17e25583bd251347c207d8ff5c.json index 347fb9b8..c61be6e5 100644 --- a/src-tauri/.sqlx/query-8c85b8c4cfd2b85ff96db3ce58ce727b3c77039d5984f523bb1923c8b81cbcd1.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 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 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": "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/.sqlx/query-d7cf32155e5dc7d775d5884014d4b9dfc7d69250bdf2799a8684fd4d93986701.json b/src-tauri/.sqlx/query-d7cf32155e5dc7d775d5884014d4b9dfc7d69250bdf2799a8684fd4d93986701.json new file mode 100644 index 00000000..ef8510dc --- /dev/null +++ b/src-tauri/.sqlx/query-d7cf32155e5dc7d775d5884014d4b9dfc7d69250bdf2799a8684fd4d93986701.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO tunnel (name, pubkey, prvkey, address, server_pubkey, allowed_ips, endpoint, dns, persistent_keep_alive, route_all_traffic, pre_up, post_up, pre_down, post_down) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id;", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 14 + }, + "nullable": [ + false + ] + }, + "hash": "d7cf32155e5dc7d775d5884014d4b9dfc7d69250bdf2799a8684fd4d93986701" +} diff --git a/src-tauri/.sqlx/query-d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2.json b/src-tauri/.sqlx/query-d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2.json new file mode 100644 index 00000000..89ac6282 --- /dev/null +++ b/src-tauri/.sqlx/query-d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2.json @@ -0,0 +1,44 @@ +{ + "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" + }, + { + "name": "check_for_updates", + "ordinal": 4, + "type_info": "Bool" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "d7e7897382881aa2f7633b86790d217b7e37fb90d7d343637bf7d00845fcfdf2" +} diff --git a/src-tauri/.sqlx/query-db268d84f76585abcbd482c6ff2eebbfd9e57e6e8b216cf55a206e9ef1a41c77.json b/src-tauri/.sqlx/query-db268d84f76585abcbd482c6ff2eebbfd9e57e6e8b216cf55a206e9ef1a41c77.json new file mode 100644 index 00000000..a7395cb6 --- /dev/null +++ b/src-tauri/.sqlx/query-db268d84f76585abcbd482c6ff2eebbfd9e57e6e8b216cf55a206e9ef1a41c77.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE settings SET theme = $1, log_level = $2, tray_icon_theme = $3, check_for_updates = $4 WHERE id = 1;", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "db268d84f76585abcbd482c6ff2eebbfd9e57e6e8b216cf55a206e9ef1a41c77" +} diff --git a/src-tauri/.sqlx/query-fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86.json b/src-tauri/.sqlx/query-e7e1186f31f01b80f2bd575e704db5377b5aa389e4e71d0aaf1f7fc5312574d2.json similarity index 54% rename from src-tauri/.sqlx/query-fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86.json rename to src-tauri/.sqlx/query-e7e1186f31f01b80f2bd575e704db5377b5aa389e4e71d0aaf1f7fc5312574d2.json index 9e297b50..f7cac7d2 100644 --- a/src-tauri/.sqlx/query-fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86.json +++ b/src-tauri/.sqlx/query-e7e1186f31f01b80f2bd575e704db5377b5aa389e4e71d0aaf1f7fc5312574d2.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?\", 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 id = $1;", "describe": { "columns": [ { @@ -9,27 +9,27 @@ "type_info": "Int64" }, { - "name": "instance_id", + "name": "name", "ordinal": 1, - "type_info": "Int64" + "type_info": "Text" }, { - "name": "name", + "name": "pubkey", "ordinal": 2, "type_info": "Text" }, { - "name": "address", + "name": "prvkey", "ordinal": 3, "type_info": "Text" }, { - "name": "pubkey", + "name": "address", "ordinal": 4, "type_info": "Text" }, { - "name": "endpoint", + "name": "server_pubkey", "ordinal": 5, "type_info": "Text" }, @@ -39,19 +39,44 @@ "type_info": "Text" }, { - "name": "dns", + "name": "endpoint", "ordinal": 7, "type_info": "Text" }, { - "name": "network_id", + "name": "dns", "ordinal": 8, + "type_info": "Text" + }, + { + "name": "persistent_keep_alive", + "ordinal": 9, "type_info": "Int64" }, { "name": "route_all_traffic", - "ordinal": 9, + "ordinal": 10, "type_info": "Bool" + }, + { + "name": "pre_up", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "post_up", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "pre_down", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "post_down", + "ordinal": 14, + "type_info": "Text" } ], "parameters": { @@ -64,11 +89,16 @@ false, false, false, + true, false, true, false, - false + false, + true, + true, + true, + true ] }, - "hash": "fec5b379a8622f5b3c3ce2c51ffefd022277cb6648d60876990645a3da8fba86" + "hash": "e7e1186f31f01b80f2bd575e704db5377b5aa389e4e71d0aaf1f7fc5312574d2" } 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/.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/Cargo.lock b/src-tauri/Cargo.lock index 4e45a946..295be74c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -23,19 +23,19 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", "once_cell", "version_check", ] [[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", + "getrandom 0.2.12", "once_cell", "version_check", "zerocopy", @@ -136,9 +136,28 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +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 = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08" +dependencies = [ + "clipboard-win", + "core-graphics", + "image", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot", + "thiserror", + "winapi", + "x11rb", +] [[package]] name = "arrayvec" @@ -163,7 +182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", - "event-listener 4.0.0", + "event-listener 4.0.3", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -175,11 +194,11 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" dependencies = [ - "async-lock 3.2.0", + "async-lock 3.3.0", "async-task", "concurrent-queue", "fastrand 2.0.1", - "futures-lite 2.1.0", + "futures-lite 2.2.0", "slab", ] @@ -221,11 +240,11 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6afaa937395a620e33dc6a742c593c01aced20aa376ffb0f628121198578ccc7" dependencies = [ - "async-lock 3.2.0", + "async-lock 3.3.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.1.0", + "futures-lite 2.2.0", "parking", "polling 3.3.1", "rustix 0.38.28", @@ -245,11 +264,11 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ - "event-listener 4.0.0", + "event-listener 4.0.3", "event-listener-strategy", "pin-project-lite", ] @@ -279,7 +298,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -319,24 +338,24 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] name = "async-task" -version = "4.6.0" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -462,9 +481,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9" [[package]] name = "base64ct" @@ -521,20 +540,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel", - "async-lock 3.2.0", + "async-lock 3.3.0", "async-task", "fastrand 2.0.1", "futures-io", - "futures-lite 2.1.0", + "futures-lite 2.2.0", "piper", "tracing", ] [[package]] name = "borsh" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d4d6dafc1a3bb54687538972158f07b2c948bc57d5890df22c0739098b3028" +checksum = "f58b559fd6448c6e2fd0adb5720cd98a2506594cafa4737ff98c396f3e82f667" dependencies = [ "borsh-derive", "cfg_aliases", @@ -542,15 +561,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0" +checksum = "7aadb5b6ccbd078890f6d7003694e33816e6b784358f18e15e7e6d9f065a57cd" dependencies = [ "once_cell", - "proc-macro-crate 2.0.0", + "proc-macro-crate 3.0.0", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "syn_derive", ] @@ -577,9 +596,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", @@ -593,9 +612,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byte-unit" -version = "5.1.2" +version = "5.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d405b41420a161b4e1dd5a52e3349f41b4dae9a39be02aff1d67fe53256430ac" +checksum = "cbda27216be70d08546aa506cecabce0c5eb0d494aaaedbd7ec82c8ae1a60b46" dependencies = [ "rust_decimal", "serde", @@ -716,9 +735,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.5" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a" dependencies = [ "smallvec", "target-lexicon", @@ -753,9 +772,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.11" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "33e92c5c1a78c62968ec57dbc2440366a2d6e5a23faf829970ff1585dc6b18e2" dependencies = [ "clap_builder", "clap_derive", @@ -763,9 +782,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "f4323769dc8a61e2c39ad7dc26f6f2800524691a44d74fe3d1071a5c24db6370" dependencies = [ "anstream", "anstyle", @@ -782,7 +801,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -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" @@ -858,6 +888,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaf16c9c2c612020bcfd042e170f6e32de9b9d75adb5277cdbbd2e2c8c8299a" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.12", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -906,9 +956,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -939,55 +989,52 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.16" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset 0.9.0", ] [[package]] name = "crossbeam-queue" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.17" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" @@ -1023,7 +1070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1033,7 +1080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" dependencies = [ "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1060,7 +1107,24 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", +] + +[[package]] +name = "dark-light" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645" +dependencies = [ + "dconf_rs", + "detect-desktop-environment", + "dirs 4.0.0", + "objc", + "rust-ini 0.18.0", + "web-sys", + "winreg 0.10.1", + "zbus", + "zvariant", ] [[package]] @@ -1084,7 +1148,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1095,48 +1159,65 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.41", + "syn 2.0.48", ] +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + [[package]] name = "defguard-client" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", - "base64 0.21.5", + "base64 0.21.6", "chrono", "clap", + "dark-light", "defguard_wireguard_rs", - "dirs", + "dirs 5.0.1", "lazy_static", "local-ip-address", "log", "nix 0.27.1", + "notify-debouncer-mini", "prost", "prost-build", "rand 0.8.5", + "reqwest", + "rust-ini 0.20.0", "serde", "serde_json", + "serde_with", "sqlx", + "struct-patch", + "strum", "tauri", "tauri-build", "tauri-plugin-log", "tauri-plugin-single-instance", "thiserror", "tokio", + "tokio-util", "tonic", "tonic-build", "tracing", + "tracing-appender", "tracing-subscriber", + "webbrowser", + "windows-service", "x25519-dalek", ] [[package]] name = "defguard_wireguard_rs" -version = "0.3.2" -source = "git+https://github.com/DefGuard/wireguard-rs.git?branch=main#b8e54ba73e281b9253b000e8d580ffc2dca979d9" +version = "0.4.0" +source = "git+https://github.com/DefGuard/wireguard-rs.git?rev=v0.4.0#2264bece5be19ff99b4f5b0558bbba0482c781af" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "libc", "log", "netlink-packet-core", @@ -1163,9 +1244,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", @@ -1195,6 +1276,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + [[package]] name = "digest" version = "0.10.7" @@ -1207,13 +1294,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", ] [[package]] @@ -1226,6 +1322,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -1255,6 +1362,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1293,11 +1415,12 @@ dependencies = [ [[package]] name = "embed-resource" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54cc3e827ee1c3812239a9a41dede7b4d7d5d5464faa32d71bd7cba28ce2cb2" +checksum = "3bde55e389bea6a966bd467ad1ad7da0ae14546a5bc794d16d1e55e7fca44881" dependencies = [ "cc", + "memchr", "rustc_version", "toml 0.8.8", "vswhom", @@ -1337,7 +1460,7 @@ checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1356,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" @@ -1386,9 +1519,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "4.0.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", @@ -1401,7 +1534,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 4.0.0", + "event-listener 4.0.3", "pin-project-lite", ] @@ -1422,9 +1555,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", ] @@ -1529,6 +1662,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "funty" version = "2.0.0" @@ -1547,9 +1689,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", @@ -1557,15 +1699,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", @@ -1585,9 +1727,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" @@ -1606,9 +1748,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" dependencies = [ "fastrand 2.0.1", "futures-core", @@ -1619,32 +1761,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.41", + "syn 2.0.48", ] [[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", @@ -1775,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" @@ -1788,9 +1940,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -1965,9 +2117,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "b553656127a00601c8ae5590fcfdc118e4083a7924b6cf4ffc1ea4b99dc429d7" dependencies = [ "bytes", "fnv", @@ -1997,7 +2149,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", ] @@ -2067,20 +2219,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "html5ever" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" -dependencies = [ - "log", - "mac", - "markup5ever 0.10.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "html5ever" version = "0.26.0" @@ -2089,7 +2227,7 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", - "markup5ever 0.11.0", + "markup5ever", "proc-macro2", "quote", "syn 1.0.109", @@ -2186,9 +2324,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", @@ -2235,9 +2373,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ "crossbeam-deque", "globset", @@ -2260,6 +2398,8 @@ dependencies = [ "color_quant", "num-rational", "num-traits", + "png", + "tiff", ] [[package]] @@ -2284,6 +2424,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" @@ -2293,6 +2442,26 @@ dependencies = [ "cfb", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -2386,12 +2555,34 @@ 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" 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" @@ -2414,15 +2605,23 @@ dependencies = [ ] [[package]] -name = "kuchiki" -version = "0.8.1" +name = "kqueue" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ - "cssparser", - "html5ever 0.25.2", - "matches", - "selectors", + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", ] [[package]] @@ -2432,7 +2631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" dependencies = [ "cssparser", - "html5ever 0.26.0", + "html5ever", "indexmap 1.9.3", "matches", "selectors", @@ -2473,9 +2672,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -2597,20 +2796,6 @@ dependencies = [ "libc", ] -[[package]] -name = "markup5ever" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" -dependencies = [ - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "string_cache", - "string_cache_codegen", - "tendril", -] - [[package]] name = "markup5ever" version = "0.11.0" @@ -2658,9 +2843,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" @@ -2709,6 +2894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2910,6 +3096,36 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-mini" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" +dependencies = [ + "crossbeam-channel", + "log", + "notify", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3030,12 +3246,23 @@ dependencies = [ ] [[package]] -name = "objc_exception" -version = "0.1.2" +name = "objc-foundation" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" dependencies = [ - "cc", + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", ] [[package]] @@ -3049,9 +3276,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", ] @@ -3064,9 +3291,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", @@ -3085,7 +3312,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -3105,9 +3332,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", @@ -3122,6 +3349,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list 0.3.0", + "hashbrown 0.12.3", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d6a8c22fc714f0c2373e6091bf6f5e9b37b1bc0b1184874b7e0a4e303d318f" +dependencies = [ + "dlv-list 0.5.2", + "hashbrown 0.14.3", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3327,7 +3574,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -3374,7 +3621,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -3423,15 +3670,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +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" @@ -3439,7 +3686,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "indexmap 2.1.0", "line-wrap", "quick-xml", @@ -3510,12 +3757,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.41", + "syn 2.0.48", ] [[package]] @@ -3530,11 +3777,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "6b2685dd208a3771337d8d386a89840f0f43cd68be8dae90a5f8c2384effc9cd" dependencies = [ - "toml_edit 0.20.7", + "toml_edit 0.21.0", ] [[package]] @@ -3569,9 +3816,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -3603,7 +3850,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.41", + "syn 2.0.48", "tempfile", "which", ] @@ -3618,7 +3865,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -3661,9 +3908,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", ] @@ -3734,7 +3981,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", ] [[package]] @@ -3776,7 +4023,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", "libredox", "thiserror", ] @@ -3840,7 +4087,7 @@ version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "bytes", "encoding_rs", "futures-core", @@ -3874,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" @@ -3923,6 +4194,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap 0.4.3", +] + +[[package]] +name = "rust-ini" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap 0.7.1", +] + [[package]] name = "rust_decimal" version = "1.33.1" @@ -4010,11 +4301,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]] @@ -4080,38 +4371,38 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa 1.0.10", "ryu", @@ -4120,20 +4411,20 @@ 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.41", + "syn 2.0.48", ] [[package]] name = "serde_spanned" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -4152,11 +4443,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +checksum = "f58c3a1b3e418f61c25b2aeb43fc6c95eaa252b8cecdda67f401943e9e08d33f" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "chrono", "hex", "indexmap 1.9.3", @@ -4169,14 +4460,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +checksum = "d2068b437a31fc68f25dd7edc296b078f04b45145c199d8eed9866e45f1ff274" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -4397,7 +4688,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", @@ -4480,7 +4771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", - "base64 0.21.5", + "base64 0.21.6", "bitflags 2.4.1", "byteorder", "bytes", @@ -4524,7 +4815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", - "base64 0.21.5", + "base64 0.21.6", "bitflags 2.4.1", "byteorder", "chrono", @@ -4604,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" @@ -4647,6 +4944,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.48", +] + +[[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.48", +] + [[package]] name = "subtle" version = "2.5.0" @@ -4666,9 +5005,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -4684,7 +5023,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -4733,7 +5072,7 @@ version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" dependencies = [ - "cfg-expr 0.15.5", + "cfg-expr 0.15.6", "heck 0.4.1", "pkg-config", "toml 0.8.8", @@ -4766,7 +5105,7 @@ dependencies = [ "gtk", "image", "instant", - "jni", + "jni 0.20.0", "lazy_static", "libappindicator", "libc", @@ -4819,15 +5158,15 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.12" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" [[package]] name = "tauri" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d563b672acde8d0cc4c1b1f5b855976923f67e8d6fe1eba51df0211e197be2" +checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af" dependencies = [ "anyhow", "bytes", @@ -4843,12 +5182,15 @@ 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", + "rfd", "semver", "serde", "serde_json", @@ -4872,9 +5214,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c" +checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532" dependencies = [ "anyhow", "cargo_toml", @@ -4891,11 +5233,11 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb" +checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "brotli", "ico", "json-patch", @@ -4916,9 +5258,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acea6445eececebd72ed7720cfcca46eee3b5bad8eb408be8f7ef2e3f7411500" +checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875" dependencies = [ "heck 0.4.1", "proc-macro2", @@ -4931,7 +5273,7 @@ dependencies = [ [[package]] name = "tauri-plugin-log" version = "0.0.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#563ca73de0f49e5ca50fba8168f350b8b6a9219b" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#775f7b88edd60df724f7e106ff748cdb967d851e" dependencies = [ "byte-unit", "fern", @@ -4946,7 +5288,7 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" version = "0.0.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#563ca73de0f49e5ca50fba8168f350b8b6a9219b" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#775f7b88edd60df724f7e106ff748cdb967d851e" dependencies = [ "log", "serde", @@ -4959,9 +5301,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43" +checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76" dependencies = [ "gtk", "http", @@ -4980,10 +5322,11 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803a01101bc611ba03e13329951a1bde44287a54234189b9024b78619c1bc206" +checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158" dependencies = [ + "arboard", "cocoa", "gtk", "percent-encoding", @@ -5000,17 +5343,17 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a52165bb340e6f6a75f1f5eeeab1bb49f861c12abe3a176067d53642b5454986" +checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db" dependencies = [ "brotli", "ctor", "dunce", "glob", "heck 0.4.1", - "html5ever 0.26.0", - "infer", + "html5ever", + "infer 0.13.0", "json-patch", "kuchikiki", "log", @@ -5040,15 +5383,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]] @@ -5070,22 +5413,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.41", + "syn 2.0.48", ] [[package]] @@ -5098,11 +5441,22 @@ 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.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "itoa 1.0.10", @@ -5122,13 +5476,22 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -5146,9 +5509,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -5181,7 +5544,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -5274,17 +5637,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_edit" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" -dependencies = [ - "indexmap 2.1.0", - "toml_datetime", - "winnow", -] - [[package]] name = "toml_edit" version = "0.21.0" @@ -5307,7 +5659,7 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.21.5", + "base64 0.21.6", "bytes", "h2", "http", @@ -5335,7 +5687,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -5382,6 +5734,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -5390,7 +5754,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -5414,6 +5778,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -5424,12 +5798,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -5539,7 +5916,7 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", ] [[package]] @@ -5550,9 +5927,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.4.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" +checksum = "7cdbaf5e132e593e9fc1de6a15bbec912395b11fb9719e061cf64f804524c503" [[package]] name = "vcpkg" @@ -5656,7 +6033,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -5690,7 +6067,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5724,6 +6101,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" @@ -5809,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" @@ -5827,6 +6227,12 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.3.9" @@ -5852,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" @@ -5893,11 +6321,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]] @@ -5916,6 +6344,26 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +[[package]] +name = "windows-service" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9db37ecb5b13762d95468a2fc6009d4b2c62801243223aabd44fca13ad13c8" +dependencies = [ + "bitflags 1.3.2", + "widestring", + "windows-sys 0.45.0", +] + +[[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" @@ -5934,6 +6382,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" @@ -5979,6 +6442,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" @@ -5991,12 +6460,24 @@ 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" 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" @@ -6009,12 +6490,24 @@ 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" 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" @@ -6027,12 +6520,24 @@ 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" 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" @@ -6045,12 +6550,24 @@ 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" 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" @@ -6063,6 +6580,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" @@ -6075,12 +6598,24 @@ 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" 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" @@ -6095,13 +6630,22 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.30" +version = "0.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.50.0" @@ -6124,9 +6668,9 @@ dependencies = [ [[package]] name = "wry" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a70547e8f9d85da0f5af609143f7bde3ac7457a6e1073104d9b73d6c5ac744" +checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4" dependencies = [ "base64 0.13.1", "block", @@ -6138,9 +6682,9 @@ dependencies = [ "gio", "glib", "gtk", - "html5ever 0.25.2", + "html5ever", "http", - "kuchiki", + "kuchikiki", "libc", "log", "objc", @@ -6190,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" @@ -6204,9 +6770,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", @@ -6291,22 +6857,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.41", + "syn 2.0.48", ] [[package]] @@ -6326,7 +6892,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 13f7650c..637c2b71 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" @@ -19,29 +19,42 @@ anyhow = "1.0" base64 = "0.21" clap = { version = "4.4", features = ["derive", "env"] } chrono = { version = "0.4", features = ["serde"] } -defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", branch = "main" } +defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.4.0" } 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.5" sqlx = { version = "0.7", features = ["chrono", "sqlite", "runtime-tokio", "uuid", "macros"] } +struct-patch = "0.4" +strum = { version = "0.25", features = ["derive"] } +dark-light = "1.0" +webbrowser = "0.8" + +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" +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"] } x25519-dalek = { version = "2", features = [ "getrandom", "static_secrets", ] } +reqwest = { version = "0.11", features = ["json"] } -tauri = { version = "1.5", features = [ "http-all", "window-all", "system-tray", "native-tls-vendored"] } -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" +[target.'cfg(target_os = "windows")'.dependencies] +windows-service = "0.6" [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..70b79f62 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -3,12 +3,21 @@ fn main() -> Result<(), Box> { let mut config = prost_build::Config::new(); // enable optional fields config.protoc_arg("--experimental_allow_proto3_optional"); + // make sure empty DNS is deserialized correctly as None + config.type_attribute(".DeviceConfig", "#[serde_as]"); + config.field_attribute( + ".DeviceConfig.dns", + "#[serde_as(deserialize_as = \"NoneAsEmptyString\")]", + ); // Make all messages serde-serializable 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/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/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/migrations/20231219114309_add_cascade_delete.sql b/src-tauri/migrations/20231219114309_add_cascade_delete.sql new file mode 100644 index 00000000..0b6b9c8a --- /dev/null +++ b/src-tauri/migrations/20231219114309_add_cascade_delete.sql @@ -0,0 +1,74 @@ +-- add on delete cascade to existing tables +PRAGMA defer_foreign_keys = ON; +PRAGMA foreign_keys=OFF; + +ALTER TABLE wireguard_keys RENAME TO wireguard_keys_old; + +CREATE TABLE wireguard_keys +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + instance_id INTEGER NOT NULL, + pubkey TEXT NOT NULL, + prvkey TEXT NOT NULL, + FOREIGN KEY (instance_id) REFERENCES instance(id) ON DELETE CASCADE +); + +ALTER TABLE location RENAME TO location_old; + +CREATE TABLE location +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + instance_id INTEGER NOT NULL, + network_id INTEGER NOT NULL, + name TEXT NOT NULL, + address TEXT NOT NULL, + pubkey TEXT NOT NULL, + endpoint TEXT NOT NULL, + allowed_ips TEXT NOT NULL, + dns TEXT, + route_all_traffic BOOLEAN NOT NULL DEFAULT false, + FOREIGN KEY (instance_id) REFERENCES instance(id) ON DELETE CASCADE +); + +ALTER TABLE location_stats RENAME TO location_stats_old; + +CREATE TABLE location_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + location_id INTEGER NOT NULL, + upload INTEGER NOT NULL, + download INTEGER NOT NULL, + last_handshake INTEGER NOT NULL, + collected_at TIMESTAMP NOT NULL, + listen_port INTEGER NOT NULL DEFAULT 0, + persistent_keepalive_interval INTEGER NULL, + FOREIGN KEY (location_id) REFERENCES location(id) ON DELETE CASCADE +); + +ALTER TABLE connection RENAME TO connection_old; + +CREATE TABLE connection ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + location_id INTEGER NOT NULL, + connected_from TEXT NOT NULL, -- Renamed from 'from' as it reserved + start TIMESTAMP NOT NULL, + end TIMESTAMP NOT NULL, + FOREIGN KEY (location_id) REFERENCES location(id) ON DELETE CASCADE +); + + +-- copy data +INSERT INTO location SELECT * FROM location_old; +INSERT INTO wireguard_keys SELECT * FROM wireguard_keys_old; +INSERT INTO connection SELECT * FROM connection_old; +INSERT INTO location_stats SELECT * FROM location_stats_old; + +-- drop old tables +DROP TABLE location_old; +DROP TABLE wireguard_keys_old; +DROP TABLE connection_old; +DROP TABLE location_stats_old; +-- restore index +CREATE INDEX idx_collected_location ON location_stats (collected_at, location_id); + +PRAGMA defer_foreign_keys = OFF; +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/src-tauri/migrations/20231221100546_create_tunnel.sql b/src-tauri/migrations/20231221100546_create_tunnel.sql new file mode 100644 index 00000000..3a1a45bd --- /dev/null +++ b/src-tauri/migrations/20231221100546_create_tunnel.sql @@ -0,0 +1,37 @@ +CREATE TABLE tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + pubkey TEXT NOT NULL, + prvkey TEXT NOT NULL, + address TEXT NOT NULL, + server_pubkey TEXT NOT NULL, + allowed_ips TEXT, + endpoint TEXT NOT NULL, + dns TEXT, + route_all_traffic BOOLEAN NOT NULL, + persistent_keep_alive INTEGER NOT NULL, + pre_up TEXT, + post_up TEXT, + pre_down TEXT, + post_down TEXT +); + +CREATE TABLE tunnel_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tunnel_id BIGINT NOT NULL, + upload BIGINT NOT NULL, + download BIGINT NOT NULL, + last_handshake BIGINT NOT NULL, + collected_at TIMESTAMP NOT NULL, + listen_port INTEGER NOT NULL, + persistent_keepalive_interval INTEGER NOT NULL, + FOREIGN KEY (tunnel_id) REFERENCES tunnel(id) ON DELETE CASCADE +); +CREATE TABLE tunnel_connection ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tunnel_id INTEGER NOT NULL, + connected_from TEXT NOT NULL, -- Renamed from 'from' as it reserved + start TIMESTAMP NOT NULL, + end TIMESTAMP NOT NULL, + FOREIGN KEY (tunnel_id) REFERENCES tunnel(id) ON DELETE CASCADE +); 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/migrations/20240115212308_add_settings_check_for_updates.sql b/src-tauri/migrations/20240115212308_add_settings_check_for_updates.sql new file mode 100644 index 00000000..365d9ddb --- /dev/null +++ b/src-tauri/migrations/20240115212308_add_settings_check_for_updates.sql @@ -0,0 +1 @@ +ALTER TABLE settings ADD COLUMN check_for_updates BOOLEAN NOT NULL DEFAULT true; diff --git a/src-tauri/proto b/src-tauri/proto index 50f3791a..dc3d6f2e 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit 50f3791a2d0104ad7d9ae69a3abde070743902c2 +Subproject commit dc3d6f2e5a376485ca5095d774edf14cece0f781 diff --git a/src-tauri/resources-windows/binaries/.gitkeep b/src-tauri/resources-windows/binaries/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src-tauri/resources-windows/binaries/wireguard-amd64-0.5.3.msi b/src-tauri/resources-windows/binaries/wireguard-amd64-0.5.3.msi new file mode 100644 index 00000000..f97ea545 Binary files /dev/null and b/src-tauri/resources-windows/binaries/wireguard-amd64-0.5.3.msi differ diff --git a/src-tauri/resources-windows/defguard-client.wxs b/src-tauri/resources-windows/defguard-client.wxs new file mode 100644 index 00000000..a045af0a --- /dev/null +++ b/src-tauri/resources-windows/defguard-client.wxs @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/resources-windows/main.wxs b/src-tauri/resources-windows/main.wxs new file mode 100644 index 00000000..768e57ce --- /dev/null +++ b/src-tauri/resources-windows/main.wxs @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + {{#if allow_downgrades}} + + {{else}} + + {{/if}} + + + Installed AND NOT UPGRADINGPRODUCTCODE + + + + + {{#if banner_path}} + + {{/if}} + {{#if dialog_image_path}} + + {{/if}} + {{#if license}} + + {{/if}} + + + + + + + + + + + + + + + + NOT REMOVE + + + + + + + + + + + WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed + + + + {{#unless license}} + + 1 + 1 + {{/unless}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#each binaries as |bin| ~}} + + + + {{/each~}} + {{#if enable_elevated_update_task}} + + + + + + + + + + {{/if}} + {{resources}} + + + + + + + + + + + + + + + + + + + + + {{#each merge_modules as |msm| ~}} + + + + + + + + {{/each~}} + + + + + + {{#each resource_file_ids as |resource_file_id| ~}} + + {{/each~}} + + {{#if enable_elevated_update_task}} + + + + {{/if}} + + + + + + + + + + + {{#each binaries as |bin| ~}} + + {{/each~}} + + + + + {{#each component_group_refs as |id| ~}} + + {{/each~}} + {{#each component_refs as |id| ~}} + + {{/each~}} + {{#each feature_group_refs as |id| ~}} + + {{/each~}} + {{#each feature_refs as |id| ~}} + + {{/each~}} + {{#each merge_refs as |id| ~}} + + {{/each~}} + + + {{#if install_webview}} + + + + + + + {{#if download_bootstrapper}} + + + + + + + {{/if}} + + + {{#if webview2_bootstrapper_path}} + + + + + + + + {{/if}} + + + {{#if webview2_installer_path}} + + + + + + + + {{/if}} + + {{/if}} + + {{#if enable_elevated_update_task}} + + + + + NOT(REMOVE) + + + + + + + (REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE + + + {{/if}} + + + + diff --git a/src-tauri/resources-windows/service-fragment.wxs b/src-tauri/resources-windows/service-fragment.wxs new file mode 100644 index 00000000..1c16bda7 --- /dev/null +++ b/src-tauri/resources-windows/service-fragment.wxs @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + 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 00000000..e0f7e171 Binary files /dev/null and b/src-tauri/resources/icons/tray-32x32-black.png differ 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 00000000..9f463510 Binary files /dev/null and b/src-tauri/resources/icons/tray-32x32-color.png differ diff --git a/src-tauri/resources/icons/tray-32x32-gray.png b/src-tauri/resources/icons/tray-32x32-gray.png new file mode 100644 index 00000000..3e5f76ea Binary files /dev/null and b/src-tauri/resources/icons/tray-32x32-gray.png differ 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 00000000..e5ede2fa Binary files /dev/null and b/src-tauri/resources/icons/tray-32x32-white.png differ diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index 1552f558..2aa651e4 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -1,22 +1,25 @@ -use std::sync::{Arc, Mutex}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; +use tokio_util::sync::CancellationToken; use tonic::transport::Channel; use crate::{ - database::{ActiveConnection, Connection, DbPool}, - 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, }; pub struct AppState { pub db: Arc>>, pub active_connections: Arc>>, pub client: DesktopDaemonServiceClient, + pub log_watchers: Arc>>, } impl Default for AppState { @@ -33,69 +36,91 @@ impl AppState { db: Arc::new(Mutex::new(None)), active_connections: Arc::new(Mutex::new(Vec::new())), client, + log_watchers: Arc::new(Mutex::new(HashMap::new())), } } 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 { + 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"); 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:#?}"); - 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); + disconnect_interface(connection, self).await?; } 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/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 14ed2d8a..95443e2d 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -7,21 +7,28 @@ use lazy_static::lazy_static; use log::{Level, LevelFilter}; #[cfg(target_os = "macos")] use tauri::{api::process, Env}; -use tauri::{Manager, State, SystemTrayEvent}; +use tauri::{Manager, State}; 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__all_tunnels, __cmd__connect, __cmd__delete_instance, __cmd__delete_tunnel, + __cmd__disconnect, __cmd__get_latest_app_version, __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, connect, disconnect, - last_connection, location_stats, save_device_config, update_instance, - update_location_routing, + active_connection, all_connections, all_instances, all_locations, all_tunnels, connect, + delete_instance, delete_tunnel, disconnect, get_latest_app_version, 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, - tray::create_tray_menu, + database::{self, models::settings::Settings}, + latest_app_version::fetch_latest_app_version_loop, + tray::{configure_tray_icon, create_tray_menu, handle_tray_event}, utils::load_log_targets, }; use std::{env, str::FromStr}; @@ -70,7 +77,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, @@ -79,10 +86,21 @@ async fn main() { disconnect, update_instance, location_stats, + location_interface_details, all_connections, last_connection, active_connection, update_location_routing, + get_settings, + update_settings, + delete_instance, + parse_tunnel_config, + save_tunnel, + all_tunnels, + open_link, + tunnel_details, + delete_tunnel, + get_latest_app_version, ]) .on_window_event(|event| match event.event() { tauri::WindowEvent::CloseRequested { api, .. } => { @@ -92,48 +110,7 @@ async fn main() { _ => {} }) .system_tray(system_tray) - .on_system_tray_event(|app, event| match event { - SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { - "quit" => { - let app_state: State = app.state(); - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { - let _ = app_state.close_all_connections().await; - std::process::exit(0); - }); - }); - } - "show" => { - if let Some(main_window) = app.get_window("main") { - if main_window - .is_minimized() - .expect("Failed to check minimization state") - { - main_window - .unminimize() - .expect("Failed to unminimize main window."); - } else if !main_window - .is_visible() - .expect("Failed to check main window visibility") - { - main_window.show().expect("Failed to show main window."); - } - } - } - "hide" => { - if let Some(main_window) = app.get_window("main") { - if main_window - .is_visible() - .expect("Failed to check main window visibility") - { - main_window.hide().expect("Failed to hide main window"); - } - } - } - _ => {} - }, - _ => {} - }) + .on_system_tray_event(handle_tray_event) .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { app.emit_all("single-instance", Payload { args: argv, cwd }) .unwrap(); @@ -159,27 +136,34 @@ 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); - }); - 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(); + } + + tauri::async_runtime::spawn( + async move { fetch_latest_app_version_loop(app_handle.clone()).await }, + ); + + // 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 b53cf38f..a50ff2f5 100644 --- a/src-tauri/src/bin/defguard-service.rs +++ b/src-tauri/src/bin/defguard-service.rs @@ -5,24 +5,25 @@ use clap::Parser; use defguard_client::service::{config::Config, run_server}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +#[cfg(not(windows))] #[tokio::main] async fn main() -> anyhow::Result<()> { - // parse config - let config = Config::parse(); + use defguard_client::service::utils::logging_setup; - // initialize tracing - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| format!("{},hyper=info", config.log_level).into()), - ) - .with(tracing_subscriber::fmt::layer()) - .init(); + // parse config + let config: Config = Config::parse(); + let _guard = logging_setup(&config); // run gRPC server run_server(config).await?; Ok(()) } + +#[cfg(windows)] +fn main() -> windows_service::Result<()> { + use defguard_client::service::windows_service::defguard_windows_service; + + defguard_windows_service::run() +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d3621795..cb62f5b8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,103 +1,85 @@ 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, + Tunnel, TunnelConnection, TunnelConnectionInfo, TunnelStats, WireguardKeys, }, error::Error, - service::proto::RemoveInterfaceRequest, - utils::{get_interface_name, setup_interface, spawn_stats_thread}, + proto::{DeviceConfig, DeviceConfigResponse}, + service::{log_watcher::stop_log_watcher_task, proto::RemoveInterfaceRequest}, + tray::configure_tray_icon, + utils::{ + disconnect_interface, 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 std::str::FromStr; +use std::{collections::HashMap, env, str::FromStr}; +use struct_patch::Patch; use tauri::{AppHandle, Manager, State}; #[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, + preshared_key: Option, + 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().unwrap().push(connection); - debug!( - "Active connections: {:#?}", - state.active_connections.lock().unwrap() - ); - 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, interface_name).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, preshared_key, 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) { + let interface_name = connection.interface_name.clone(); debug!("Found active connection"); trace!("Connection: {:#?}", connection); - debug!("Removing interface"); - let mut client = state.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); - let mut connection: Connection = connection.into(); - connection.save(&state.get_pool()).await?; + disconnect_interface(connection, &state).await?; debug!("Connection saved"); - trace!("Saved connection: {connection:#?}"); handle.emit_all( "connection-changed", Payload { message: "Created new connection".into(), }, )?; - info!("Location {} disconnected", connection.location_id); + stop_log_watcher_task(handle, interface_name)?; 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, @@ -107,18 +89,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 { @@ -132,6 +102,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)] @@ -142,13 +114,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, @@ -158,32 +123,42 @@ 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 mut keys = WireguardKeys::new(instance.id.unwrap(), response.device.pubkey, private_key); + let device = response + .device + .expect("Missing device info in device config response"); + let mut keys = WireguardKeys::new( + instance.id.expect("Missing instance ID"), + device.pubkey, + private_key, + ); keys.save(&mut *transaction).await?; for location in response.configs { - let mut new_location = device_config_to_location(location, instance.id.unwrap()); + let mut new_location = + device_config_to_location(location, instance.id.expect("Missing instance ID")); new_location.save(&mut *transaction).await?; } transaction.commit().await?; info!("Instance created."); trace!("Created following instance: {instance:#?}"); - let locations = - Location::find_by_instance_id(&app_state.get_pool(), instance.id.unwrap()).await?; + let locations = Location::find_by_instance_id( + &app_state.get_pool(), + instance.id.expect("Missing instance ID"), + ) + .await?; trace!("Created following locations: {locations:#?}"); handle.emit_all("instance-update", ())?; let res: SaveDeviceConfigResponse = SaveDeviceConfigResponse { @@ -198,17 +173,11 @@ pub async fn all_instances(app_state: State<'_, AppState>) -> Result = vec![]; - let connection_ids: Vec = app_state - .active_connections - .lock() - .unwrap() - .iter() - .map(|connection| connection.location_id) - .collect(); - for instance in &instances { + 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; }; @@ -222,13 +191,14 @@ pub async fn all_instances(app_state: State<'_, AppState>) -> Result) -> Result 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() - .unwrap() - .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 { - id: location.id.unwrap(), + id: location.id.expect("Missing location ID"), instance_id: location.instance_id, name: location.name, address: location.address, endpoint: location.endpoint, - active: active_locations_ids.contains(&location.id.unwrap()), + active: active_locations_ids.contains(&location.id.expect("Missing location ID")), route_all_traffic: location.route_all_traffic, + connection_type: ConnectionType::Location, + pubkey: location.pubkey, + mfa_enabled: location.mfa_enabled, + network_id: location.network_id, }; location_info.push(info); } debug!( - "Returning {} locations for instance {}", + "Returning {} locations for instance {instance_id}", location_info.len(), - instance_id ); trace!("Locations returned:\n{location_info:#?}"); Ok(location_info) } + +#[derive(Serialize, Debug)] +pub struct LocationInterfaceDetails { + pub location_id: i64, + // client interface config + pub name: String, // interface name generated from location name + pub pubkey: String, // own pubkey of client interface + pub address: String, // IP within WireGuard network assigned to the client + pub dns: Option, + pub listen_port: Option, + // peer config + pub peer_pubkey: String, + pub peer_endpoint: String, + pub allowed_ips: String, + pub persistent_keepalive_interval: Option, + pub last_handshake: Option, +} + +#[tauri::command(async)] +pub async fn location_interface_details( + location_id: i64, + connection_type: ConnectionType, + app_state: State<'_, AppState>, +) -> Result { + let pool = app_state.get_pool(); + match connection_type { + ConnectionType::Location => get_location_interface_details(location_id, &pool).await, + ConnectionType::Tunnel => get_tunnel_interface_details(location_id, &pool).await, + } +} + #[tauri::command(async)] pub async fn update_instance( instance_id: i64, - response: CreateDeviceResponse, + response: DeviceConfigResponse, app_state: State<'_, AppState>, + app_handle: AppHandle, ) -> Result<(), Error> { debug!("Received update_instance command"); trace!("Processing following response:\n {response:#?}"); + let pool = app_state.get_pool(); - 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; + if let Some(mut instance) = Instance::find_by_id(&pool, instance_id).await? { + // fetch existing locations for given instance + let mut current_locations = Location::find_by_instance_id(&pool, instance_id).await?; + + let mut transaction = pool.begin().await?; + + // update instance + 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?; + // process locations received in response for location in response.configs { + // parse device config let mut new_location = device_config_to_location(location, instance_id); - let old_location = - Location::find_by_native_id(&mut *transaction, new_location.network_id).await?; - if let Some(mut old_location) = old_location { - old_location.name = new_location.name; - old_location.address = new_location.address; - old_location.pubkey = new_location.pubkey; - old_location.endpoint = new_location.endpoint; - old_location.allowed_ips = new_location.allowed_ips; - old_location.save(&mut *transaction).await?; + + // check if location is already present in current locations + if let Some(position) = current_locations + .iter() + .position(|loc| loc.network_id == new_location.network_id) + { + // remove from list of existing locations + let mut current_location = current_locations.remove(position); + // update existing location + current_location.name = new_location.name; + current_location.address = new_location.address; + current_location.pubkey = new_location.pubkey; + current_location.endpoint = new_location.endpoint; + current_location.allowed_ips = new_location.allowed_ips; + current_location.mfa_enabled = new_location.mfa_enabled; + current_location.keepalive_interval = new_location.keepalive_interval; + current_location.save(&mut *transaction).await?; } else { + // create new location new_location.save(&mut *transaction).await?; } } + + // remove locations which were present in current locations + // but no longer found in core response + for removed_location in current_locations { + removed_location.delete(&mut *transaction).await?; + } + transaction.commit().await?; - info!("Instance {} updated", instance_id); + + info!("Instance {instance_id} updated"); + app_handle.emit_all("instance-update", ())?; Ok(()) } else { Err(Error::NotFound) @@ -325,7 +358,7 @@ pub async fn update_instance( } /// If `datetime` is Some, parses the date string, otherwise returns `DateTime` one hour ago. -fn parse_timestamp(from: Option) -> Result, Error> { +pub(crate) fn parse_timestamp(from: Option) -> Result, Error> { Ok(match from { Some(from) => DateTime::::from_str(&from).map_err(|_| Error::Datetime)?, None => Utc::now() - Duration::hours(1), @@ -362,23 +395,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) @@ -387,58 +469,316 @@ 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.unwrap()); - if connection.is_some() { - debug!("Active connection found"); - } - trace!("Connection:\n{:#?}", connection); - debug!("Connection returned"); - Ok(connection) - } else { - error!("Location with id: {} not found.", location_id); - 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) + 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) + } + } + } +} + +#[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(); + trace!("Pool received"); + let mut settings = Settings::get(pool).await?; + trace!("Settings read from table"); + settings.apply(data); + debug!("Saving settings"); + settings.save(pool).await?; + 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) +} + +#[tauri::command(async)] +pub async fn delete_instance(instance_id: i64, handle: AppHandle) -> Result<(), Error> { + debug!("Deleting instance {instance_id}"); + let app_state = handle.state::(); + let mut client = app_state.client.clone(); + let pool = &app_state.get_pool(); + if let Some(instance) = Instance::find_by_id(pool, instance_id).await? { + 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, &ConnectionType::Location) + { + 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) + .await + .map_err(|_| Error::InternalError)?; + debug!("Connection closed and interface removed"); + } + } + } + instance.delete(pool).await?; + } else { + error!("Instance {instance_id} not found"); + return Err(Error::NotFound); + } + handle.emit_all("instance-update", ())?; + info!("Instance {instance_id}, deleted"); + Ok(()) +} +#[tauri::command(async)] +pub async fn parse_tunnel_config(config: String) -> Result { + debug!("Parsing config file"); + parse_wireguard_config(&config).map_err(|error| { + error!("{error}"); + Error::ConfigParseError(error.to_string()) + }) +} +#[tauri::command(async)] +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(()) +} + +#[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, + pub connection_type: ConnectionType, +} + +#[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!("Tunnels found: {tunnels:#?}"); + let mut tunnel_info: Vec = vec![]; + let active_tunnel_ids: Vec = app_state.get_connection_id_by_type(&ConnectionType::Tunnel); + + 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: active_tunnel_ids.contains(&tunnel.id.expect("Missing Tunnel ID")), + connection_type: ConnectionType::Tunnel, + }) + } + Ok(tunnel_info) +} +#[tauri::command(async)] +pub async fn tunnel_details( + tunnel_id: i64, + app_state: State<'_, AppState>, +) -> 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!("Location with id: {} not found.", location_id); + error!("Tunnel with ID: {tunnel_id}, not found"); Err(Error::NotFound) } } + +#[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) { + Ok(_) => Ok(()), + Err(e) => Err(Error::CommandError(e.to_string())), + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct AppVersionInfo { + pub version: String, + pub release_date: String, + pub release_notes_url: String, + pub update_url: String, +} + +static PRODUCT_NAME: &str = "defguard-client"; + +#[tauri::command(async)] +pub async fn get_latest_app_version(handle: AppHandle) -> Result { + let app_version = handle.package_info().version.to_string(); + let current_version = app_version.as_str(); + let operating_system = env::consts::OS; + + let mut request_data = HashMap::new(); + request_data.insert("product", PRODUCT_NAME); + request_data.insert("client_version", current_version); + request_data.insert("operating_system", operating_system); + + info!("Fetching latest application version with args: current version {current_version} and operating system {operating_system}"); + + let client = reqwest::Client::new(); + let res = client + .post("https://pkgs.defguard.net/api/update/check") + .json(&request_data) + .send() + .await; + + if let Ok(response) = res { + let response_json: Result = + response.json::().await; + + response_json.map_err(|err| { + error!("Failed to deserialize latest application version response {err}"); + Error::CommandError(err.to_string()) + }) + } else { + let err = res.err().unwrap(); + error!("Failed to fetch latest application version {err}"); + Err(Error::CommandError(err.to_string())) + } +} diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 05cb02da..e0dca6d5 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -38,9 +38,14 @@ 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?; info!("Applied migrations."); Ok(pool) } @@ -62,5 +67,7 @@ pub use models::{ connection::{ActiveConnection, Connection, ConnectionInfo}, instance::{Instance, InstanceInfo}, location::{Location, LocationStats}, + settings::{Settings, SettingsLogLevel, SettingsTheme, TrayIconTheme}, + tunnel::{Tunnel, TunnelConnection, TunnelConnectionInfo, TunnelStats}, wireguard_keys::WireguardKeys, }; diff --git a/src-tauri/src/database/models/connection.rs b/src-tauri/src/database/models/connection.rs index 4ce71261..581e9fb6 100644 --- a/src-tauri/src/database/models/connection.rs +++ b/src-tauri/src/database/models/connection.rs @@ -2,7 +2,9 @@ use chrono::{NaiveDateTime, Utc}; use serde::Serialize; use sqlx::{query, query_as, FromRow}; -use crate::{database::DbPool, error::Error}; +use crate::{ + database::DbPool, error::Error, CommonConnection, CommonConnectionInfo, ConnectionType, +}; #[derive(FromRow, Debug, Serialize, Clone)] pub struct Connection { @@ -77,6 +79,19 @@ pub struct ConnectionInfo { pub upload: Option, 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/instance.rs b/src-tauri/src/database/models/instance.rs index 89e37bf7..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,22 +84,43 @@ 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) .await?; Ok(instance) } + + pub async fn delete_by_id(pool: &DbPool, id: i64) -> Result<(), Error> { + // delete instance + query!("DELETE FROM instance WHERE id = $1", id) + .execute(pool) + .await?; + Ok(()) + } + + pub async fn delete(&self, pool: &DbPool) -> Result<(), Error> { + match self.id { + Some(id) => { + Instance::delete_by_id(pool, id).await?; + Ok(()) + } + None => Err(Error::NotFound), + } + } } #[derive(FromRow, Debug, Serialize, Deserialize)] @@ -79,6 +129,7 @@ pub struct InstanceInfo { pub name: String, pub uuid: String, pub url: String, - pub connected: bool, + 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 6c7b286f..88fe47e2 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -1,8 +1,14 @@ use chrono::{NaiveDateTime, Utc}; use sqlx::{query, query_as, Error as SqlxError, FromRow}; -use std::time::SystemTime; +use std::{ + fmt::{Display, Formatter}, + 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}; @@ -19,6 +25,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)] @@ -29,9 +37,31 @@ 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 { +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, + pool: &DbPool, +) -> Result { let location = Location::find_by_public_key(pool, &peer.public_key.to_string()).await?; Ok(LocationStats { id: None, @@ -43,40 +73,26 @@ pub async fn peer_to_location_stats(peer: &Peer, pool: &DbPool) -> Result, - ) -> Self { - Location { - id: None, - instance_id, - network_id, - name, - address, - pubkey, - endpoint, - allowed_ips, - dns, - route_all_traffic: false, +impl Display for Location { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.id { + Some(location_id) => write!(f, "[ID {location_id}] {}", self.name), + None => write!(f, "{}", self.name), } } +} +impl Location { 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) @@ -92,18 +108,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?; @@ -112,18 +130,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?; @@ -136,7 +156,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 ) @@ -150,7 +171,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 ) @@ -161,7 +183,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 ) @@ -169,22 +192,17 @@ impl Location { .await } - pub async fn find_by_native_id<'e, E>( - executor: E, - instance_id: i64, - ) -> Result, SqlxError> + pub async fn delete<'e, E>(&self, executor: E) -> Result<(), SqlxError> where E: sqlx::Executor<'e, Database = sqlx::Sqlite>, { - query_as!( - Self, - "SELECT id \"id?\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic \ - FROM location WHERE network_id = $1;", - instance_id - ) - .fetch_optional(executor) - - .await + info!("Removing location {self}"); + if let Some(id) = self.id { + query!("DELETE FROM location WHERE id = $1;", id) + .execute(executor) + .await?; + } + Ok(()) } } @@ -196,6 +214,8 @@ impl LocationStats { download: i64, last_handshake: i64, collected_at: NaiveDateTime, + listen_port: u32, + persistent_keepalive_interval: Option, ) -> Self { LocationStats { id: None, @@ -204,19 +224,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 +259,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/database/models/mod.rs b/src-tauri/src/database/models/mod.rs index 0fc7d2b8..93155f45 100644 --- a/src-tauri/src/database/models/mod.rs +++ b/src-tauri/src/database/models/mod.rs @@ -1,4 +1,6 @@ pub mod connection; pub mod instance; pub mod location; +pub mod settings; +pub mod tunnel; 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..761e2a4a --- /dev/null +++ b/src-tauri/src/database/models/settings.rs @@ -0,0 +1,126 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use sqlx::{query, FromRow, Type}; +use struct_patch::Patch; +use strum::{AsRefStr, EnumString}; +use tracing::Level; + +use crate::{database::DbPool, error::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Type, EnumString, AsRefStr)] +#[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, +} + +impl From for Level { + fn from(val: SettingsLogLevel) -> Self { + match val { + SettingsLogLevel::Error => Self::ERROR, + SettingsLogLevel::Info => Self::INFO, + SettingsLogLevel::Debug => Self::DEBUG, + SettingsLogLevel::Trace => Self::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, + pub check_for_updates: bool, +} + +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)?, + check_for_updates: query_res.check_for_updates, + }; + 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, check_for_updates = $4 \ + WHERE id = 1;", + self.theme, + self.log_level, + self.tray_icon_theme, + self.check_for_updates, + ) + .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() { + debug!("No settings found on app init."); + let mut init_theme = SettingsTheme::Light; + // check what system theme is currently in use and default to it. + if dark_light::detect() == dark_light::Mode::Dark { + debug!("Detected system theme dark, init theme ajusted."); + init_theme = SettingsTheme::Dark; + }; + let default_settings = Settings { + id: None, + log_level: SettingsLogLevel::Info, + theme: init_theme, + tray_icon_theme: TrayIconTheme::Color, + check_for_updates: true, + }; + query!( + "INSERT INTO settings (log_level, theme, tray_icon_theme, check_for_updates) VALUES ($1, $2, $3, $4);", + default_settings.log_level, + default_settings.theme, + default_settings.tray_icon_theme, + default_settings.check_for_updates, + ) + .execute(pool) + .await?; + } + Ok(()) + } +} diff --git a/src-tauri/src/database/models/tunnel.rs b/src-tauri/src/database/models/tunnel.rs new file mode 100644 index 00000000..cecf1130 --- /dev/null +++ b/src-tauri/src/database/models/tunnel.rs @@ -0,0 +1,472 @@ +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}; +use serde_with::{serde_as, NoneAsEmptyString}; +use sqlx::{query, query_as, Error as SqlxError, FromRow}; +use std::time::SystemTime; + +#[serde_as] +#[derive(Debug, FromRow, Serialize, Deserialize)] +pub struct Tunnel { + pub id: Option, + pub name: String, + // user keys + pub pubkey: String, + pub prvkey: String, + // server config + pub address: String, + pub server_pubkey: String, + pub allowed_ips: Option, + // server_address:port + pub endpoint: String, + #[serde_as(deserialize_as = "NoneAsEmptyString")] + pub dns: Option, + pub persistent_keep_alive: i64, // New field + pub route_all_traffic: bool, + // additional commands + pub pre_up: Option, + pub post_up: Option, + pub pre_down: Option, + pub post_down: Option, +} + +impl Tunnel { + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn new( + name: String, + pubkey: String, + prvkey: String, + address: String, + server_pubkey: String, + allowed_ips: Option, + endpoint: String, + dns: Option, + persistent_keep_alive: i64, + route_all_traffic: bool, + pre_up: Option, + post_up: Option, + pre_down: Option, + post_down: Option, + ) -> Self { + Tunnel { + id: None, + name, + pubkey, + prvkey, + address, + server_pubkey, + allowed_ips, + endpoint, + dns, + persistent_keep_alive, + route_all_traffic, + pre_up, + post_up, + pre_down, + post_down, + } + } + + pub async fn save(&mut self, pool: &DbPool) -> Result<(), SqlxError> { + match self.id { + None => { + // Insert a new record when there is no ID + let result = query!( + "INSERT INTO tunnel (name, pubkey, prvkey, address, server_pubkey, allowed_ips, \ + endpoint, dns, persistent_keep_alive, route_all_traffic, pre_up, post_up, pre_down, post_down) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id;", + self.name, + self.pubkey, + self.prvkey, + self.address, + self.server_pubkey, + self.allowed_ips, + self.endpoint, + self.dns, + self.persistent_keep_alive, + self.route_all_traffic, + self.pre_up, + self.post_up, + self.pre_down, + self.post_down, + ) + .fetch_one(pool) + .await?; + self.id = Some(result.id); + } + Some(id) => { + // Update the existing record when there is an ID + query!( + "UPDATE tunnel SET name = $1, pubkey = $2, prvkey = $3, address = $4, \ + server_pubkey = $5, allowed_ips = $6, endpoint = $7, dns = $8, \ + persistent_keep_alive = $9, route_all_traffic = $10, pre_up = $11, post_up = $12, pre_down = $13, post_down = $14 \ + WHERE id = $15;", + self.name, + self.pubkey, + self.prvkey, + self.address, + self.server_pubkey, + self.allowed_ips, + self.endpoint, + self.dns, + self.persistent_keep_alive, + self.route_all_traffic, + self.pre_up, + self.post_up, + self.pre_down, + self.post_down, + id, + ) + .execute(pool) + .await?; + } + } + + Ok(()) + } + + pub async fn find_by_id(pool: &DbPool, tunnel_id: i64) -> Result, SqlxError> { + query_as!( + Self, + "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 id = $1;", + tunnel_id + ) + .fetch_optional(pool) + .await + } + + pub async fn all(pool: &DbPool) -> Result, SqlxError> { + let tunnels = query_as!( + Self, + "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;" + ) + .fetch_all(pool) + .await?; + Ok(tunnels) + } + 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 server_pubkey = $1;", + pubkey + ) + .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)] +pub struct TunnelStats { + id: Option, + tunnel_id: i64, + upload: i64, + download: i64, + last_handshake: i64, + collected_at: NaiveDateTime, + listen_port: u32, + persistent_keepalive_interval: Option, +} + +impl TunnelStats { + #[must_use] + pub fn new( + tunnel_id: i64, + upload: i64, + download: i64, + last_handshake: i64, + collected_at: NaiveDateTime, + listen_port: u32, + persistent_keepalive_interval: Option, + ) -> Self { + TunnelStats { + id: None, + tunnel_id, + upload, + download, + last_handshake, + collected_at, + listen_port, + persistent_keepalive_interval, + } + } + + pub async fn save(&mut self, pool: &DbPool) -> Result<(), SqlxError> { + let result = query!( + "INSERT INTO tunnel_stats (tunnel_id, upload, download, last_handshake, collected_at, listen_port, persistent_keepalive_interval) \ + VALUES ($1, $2, $3, $4, $5, $6, $7) \ + RETURNING id;", + self.tunnel_id, + self.upload, + self.download, + self.last_handshake, + self.collected_at, + self.listen_port, + self.persistent_keepalive_interval, + ) + .fetch_one(pool) + .await?; + self.id = Some(result.id); + Ok(()) + } + + pub async fn all_by_tunnel_id( + pool: &DbPool, + tunnel_id: i64, + from: &NaiveDateTime, + aggregation: &DateTimeAggregation, + ) -> Result, SqlxError> { + let aggregation = aggregation.fstring(); + let stats = query_as!( + TunnelStats, + r#" + WITH cte AS ( + SELECT + id, tunnel_id, + COALESCE(upload - LAG(upload) OVER (PARTITION BY tunnel_id ORDER BY collected_at), 0) as upload, + COALESCE(download - LAG(download) OVER (PARTITION BY tunnel_id ORDER BY collected_at), 0) as download, + last_handshake, strftime($1, collected_at) as collected_at, listen_port, persistent_keepalive_interval + FROM tunnel_stats + ORDER BY collected_at + LIMIT -1 OFFSET 1 + ) + SELECT + id, tunnel_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 tunnel_id = $2 + AND collected_at >= $3 + GROUP BY collected_at + ORDER BY collected_at; + "#, + aggregation, + tunnel_id, + from + ) + .fetch_all(pool) + .await?; + Ok(stats) + } +} +pub async fn peer_to_tunnel_stats( + peer: &Peer, + listen_port: u32, + pool: &DbPool, +) -> Result { + let tunnel = Tunnel::find_by_server_public_key(pool, &peer.public_key.to_string()).await?; + Ok(TunnelStats { + id: None, + tunnel_id: tunnel.id.unwrap(), + upload: peer.tx_bytes as i64, + download: peer.rx_bytes as i64, + last_handshake: peer.last_handshake.map_or(0, |ts| { + ts.duration_since(SystemTime::UNIX_EPOCH) + .map_or(0, |duration| duration.as_secs() as i64) + }), + collected_at: Utc::now().naive_utc(), + listen_port, + 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 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!( + "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 latest_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) + } +} + +/// 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/error.rs b/src-tauri/src/error.rs index b3cbb54c..384a281f 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -34,6 +34,16 @@ 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), + #[error("Config parse error {0}")] + 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-tauri/src/latest_app_version.rs b/src-tauri/src/latest_app_version.rs new file mode 100644 index 00000000..dfc0c434 --- /dev/null +++ b/src-tauri/src/latest_app_version.rs @@ -0,0 +1,39 @@ +use std::time::Duration; +use tauri::{AppHandle, Manager}; +use tokio::time::sleep; + +use crate::{appstate::AppState, commands::get_latest_app_version, database::Settings}; + +const INTERVAL_IN_SECONDS: Duration = Duration::from_secs(12 * 60 * 60); // 12 hours + +pub async fn fetch_latest_app_version_loop(app_handle: AppHandle) { + let state = app_handle.state::(); + let pool = &state.get_pool(); + + loop { + debug!("Waiting to fetch latest application version"); + sleep(INTERVAL_IN_SECONDS).await; + + let settings = Settings::get(pool).await; + + if let Ok(settings) = settings { + if settings.check_for_updates { + let response = get_latest_app_version(app_handle.clone()).await; + + if let Ok(result) = response { + debug!("Fetched latest application version info: {result:?}"); + + let _ = app_handle.emit_all("app-version-fetch", &result); + } else { + let err = response.err().unwrap(); + error!("Error while fetching latest application version: {err}"); + } + } else { + debug!("Checking for updates is turned off. Skipping latest application version fetch."); + } + } else { + let err = settings.err().unwrap(); + error!("Error while fetching settings: {err}"); + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 81f682a0..f1621a1e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,18 @@ +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; pub mod appstate; pub mod commands; pub mod database; pub mod error; +pub mod latest_app_version; pub mod service; pub mod tray; pub mod utils; +pub mod wg_config; + +pub mod proto { + tonic::include_proto!("enrollment"); +} #[derive(Clone, serde::Serialize)] struct Payload { @@ -12,5 +20,63 @@ struct Payload { cwd: String, } +/// Location type used in commands to check if we using tunnel or location +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub enum ConnectionType { + Tunnel, + Location, +} + #[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 new file mode 100644 index 00000000..58af5fd7 --- /dev/null +++ b/src-tauri/src/service/log_watcher.rs @@ -0,0 +1,343 @@ +//! Log watcher for observing and parsing `defguard-service` log files +//! +//! This is meant to handle passing relevant logs from `defguard-service` daemon to the client GUI. +//! 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, ConnectionType}; +use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; +use notify_debouncer_mini::{ + new_debouncer, + notify::{self, RecursiveMode}, +}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use std::{ + fs::{read_dir, File}, + io::{BufRead, BufReader}, + path::PathBuf, + str::FromStr, + time::{Duration, SystemTime}, +}; +use tauri::{async_runtime::TokioJoinHandle, AppHandle, Manager}; +use thiserror::Error; +use tokio_util::sync::CancellationToken; +use tracing::Level; + +#[derive(Error, Debug)] +pub enum LogWatcherError { + #[error(transparent)] + TauriError(#[from] tauri::Error), + #[error(transparent)] + SerdeJsonError(#[from] serde_json::Error), + #[error(transparent)] + NotifyError(#[from] notify::Error), + #[error(transparent)] + IoError(#[from] std::io::Error), +} + +/// Represents a single line in log file +#[serde_as] +#[derive(Clone, Debug, Deserialize, Serialize)] +struct LogLine { + timestamp: DateTime, + #[serde_as(as = "DisplayFromStr")] + level: Level, + target: String, + fields: LogLineFields, + span: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Span { + interface_name: Option, + name: Option, + peer: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct LogLineFields { + message: String, +} + +#[derive(Debug)] +pub struct ServiceLogWatcher { + interface_name: String, + log_level: Level, + from: Option>, + log_dir: PathBuf, + current_log_file: Option, + current_position: u64, + handle: AppHandle, + cancellation_token: CancellationToken, + event_topic: String, +} + +impl ServiceLogWatcher { + #[must_use] + pub fn new( + handle: AppHandle, + cancellation_token: CancellationToken, + event_topic: String, + interface_name: String, + log_level: Level, + from: Option>, + ) -> Self { + // get log file directory + let log_dir = get_service_log_dir(); + info!("Log dir: {log_dir:?}"); + Self { + interface_name, + log_level, + from, + log_dir, + current_log_file: None, + current_position: 0, + handle, + cancellation_token, + event_topic, + } + } + + /// Run the log watcher + /// + /// Setup a directory watcher with a 2 second debounce and parse the log dir on each change. + pub fn run(&mut self) -> Result<(), LogWatcherError> { + // setup debouncer + let (tx, rx) = std::sync::mpsc::channel(); + let mut debouncer = new_debouncer(Duration::from_secs(2), tx)?; + + debouncer + .watcher() + .watch(&self.log_dir, RecursiveMode::Recursive)?; + + // parse log dir initially before watching for changes + self.parse_log_dir()?; + + for result in rx { + if self.cancellation_token.is_cancelled() { + info!( + "Received cancellation request. Stopping log watcher for interface {}", + self.interface_name + ); + break; + } + match result { + Ok(_events) => { + self.parse_log_dir()?; + } + Err(error) => println!("Error {error:?}"), + } + } + Ok(()) + } + + /// Parse the log file directory + /// + /// Analyzing the directory consists of finding the latest log file, + /// parsing log lines and emitting tauri events whenever relevant logs are found. + /// Current log file and latest read position are stored between runs + /// so only new log lines are sent to the frontend whenever a change in + /// the directory is detected. + fn parse_log_dir(&mut self) -> Result<(), LogWatcherError> { + // get latest log file + let latest_log_file = self.get_latest_log_file()?; + info!("found latest log file: {latest_log_file:?}"); + + // check if latest file changed + if latest_log_file.is_some() && latest_log_file != self.current_log_file { + self.current_log_file = latest_log_file; + // reset read position + self.current_position = 0; + } + + // read and parse file from last position + if let Some(log_file) = &self.current_log_file { + let file = File::open(log_file)?; + let size = file.metadata()?.len(); + let mut reader = BufReader::new(file); + reader.seek_relative(self.current_position as i64)?; + let mut parsed_lines = Vec::new(); + for line in reader.lines() { + let line = line?; + if let Some(parsed_line) = self.parse_log_line(line)? { + parsed_lines.push(parsed_line); + } + } + // emit event with all relevant log lines + if !parsed_lines.is_empty() { + self.handle.emit_all(&self.event_topic, parsed_lines)?; + } + + // update read position to end of file + self.current_position = size; + } + Ok(()) + } + + /// Parse a service log line + /// + /// Deserializes the log line into a known struct and checks if the line is relevant + /// to the specified interface. Also performs filtering by log level and optional timestamp. + fn parse_log_line(&self, line: String) -> Result, LogWatcherError> { + debug!("Parsing log line: {line}"); + let log_line = serde_json::from_str::(&line)?; + debug!("Parsed log line into: {log_line:?}"); + + // filter by log level + if log_line.level > self.log_level { + debug!( + "Log level {} is above configured verbosity threshold {}. Skipping line...", + log_line.level, self.log_level + ); + return Ok(None); + } + + // filter by optional timestamp + if let Some(from) = self.from { + if log_line.timestamp < from { + debug!("Timestamp is before configured threshold {from}. Skipping line..."); + return Ok(None); + } + } + + // publish all log lines with a matching interface name or with no interface name specified + if let Some(ref span) = log_line.span { + if let Some(interface_name) = &span.interface_name { + if interface_name != &self.interface_name { + debug!("Interface name {interface_name} is not the configured name {}. Skipping line...", self.interface_name); + return Ok(None); + } + } + } + + Ok(Some(log_line)) + } + + /// Find the latest log file in directory + /// + /// Log files are rotated daily and have a knows naming format, + /// with the last 10 characters specifying a date (e.g. `2023-12-15`). + fn get_latest_log_file(&self) -> Result, LogWatcherError> { + debug!("Getting latest log file"); + let entries = read_dir(&self.log_dir)?; + + let mut latest_log = None; + let mut latest_time = SystemTime::UNIX_EPOCH; + for entry in entries.flatten() { + // skip directories + if entry.metadata()?.is_file() { + let filename = entry.file_name().to_string_lossy().into_owned(); + if let Some(timestamp) = extract_timestamp(&filename) { + if timestamp > latest_time { + latest_time = timestamp; + latest_log = Some(entry.path()); + } + } + } + } + Ok(latest_log) + } +} + +fn extract_timestamp(filename: &str) -> Option { + debug!("Extracting timestamp from log file name: {filename}"); + // we know that the date is always in the last 10 characters + let split_pos = filename.char_indices().nth_back(9)?.0; + let timestamp = &filename[split_pos..]; + // parse and convert to `SystemTime` + if let Ok(timestamp) = NaiveDate::parse_from_str(timestamp, "%Y-%m-%d") { + let timestamp = timestamp.and_time(NaiveTime::default()).timestamp(); + return Some(SystemTime::UNIX_EPOCH + Duration::from_secs(timestamp as u64)); + } + None +} + +/// Starts a log watcher in a separate thread +/// +/// The watcher parses `defguard-service` log files and extracts logs relevant +/// to the WireGuard interface for a given location. +/// Logs are then transmitted to the frontend by using `tauri` `Events`. +/// Returned value is the name of an event topic to monitor. +pub async fn spawn_log_watcher_task( + handle: AppHandle, + location_id: i64, + interface_name: String, + connection_type: ConnectionType, + log_level: Level, + from: Option, +) -> Result { + info!("Spawning log watcher task for location ID {location_id}, interface {interface_name}"); + let app_state = handle.state::(); + + // parse `from` timestamp + let from = from.and_then(|from| DateTime::::from_str(&from).ok()); + + 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(); + let interface_name_clone = interface_name.clone(); + let handle_clone = handle.clone(); + + // prepare cancellation token + let token = CancellationToken::new(); + let token_clone = token.clone(); + + // spawn task + let _join_handle: TokioJoinHandle> = tokio::spawn(async move { + let mut log_watcher = ServiceLogWatcher::new( + handle_clone, + token_clone, + topic_clone, + interface_name_clone, + log_level, + from, + ); + log_watcher.run()?; + Ok(()) + }); + + // store `CancellationToken` to manually stop watcher thread + let mut log_watchers = app_state + .log_watchers + .lock() + .expect("Failed to lock log watchers mutex"); + if let Some(old_token) = log_watchers.insert(interface_name.clone(), token) { + // cancel previous log watcher for this interface + debug!("Existing log watcher for interface {interface_name} found. Cancelling..."); + old_token.cancel(); + } + + Ok(event_topic) +} + +/// Stops the log watcher thread +pub fn stop_log_watcher_task(handle: AppHandle, interface_name: String) -> Result<(), Error> { + info!("Stopping log watcher task for interface {interface_name}"); + let app_state = handle.state::(); + + // get `CancellationToken` to manually stop watcher thread + let mut log_watchers = app_state + .log_watchers + .lock() + .expect("Failed to lock log watchers mutex"); + + match log_watchers.remove(&interface_name) { + Some(token) => { + debug!("Using cancellation token for log watcher on interface {interface_name}"); + token.cancel(); + Ok(()) + } + None => { + error!("Log watcher for interface {interface_name} not found."); + Err(Error::NotFound) + } + } +} diff --git a/src-tauri/src/service/mod.rs b/src-tauri/src/service/mod.rs index e4e29098..02d324fb 100644 --- a/src-tauri/src/service/mod.rs +++ b/src-tauri/src/service/mod.rs @@ -2,7 +2,10 @@ pub mod config; pub mod proto { tonic::include_proto!("client"); } +pub mod log_watcher; pub mod utils; +#[cfg(windows)] +pub mod windows_service; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, @@ -22,10 +25,10 @@ use tonic::{ transport::Server, Code, Response, Status, }; -use tracing::{debug, info}; +use tracing::{debug, error, info, info_span, Instrument}; use self::config::Config; -use crate::utils::IS_MACOS; +use crate::utils::{execute_command, IS_MACOS}; use proto::{ desktop_daemon_service_server::{DesktopDaemonService, DesktopDaemonServiceServer}, @@ -45,7 +48,7 @@ pub enum DaemonError { TransportError(#[from] tonic::transport::Error), } -#[derive(Default)] +#[derive(Debug, Default)] pub struct DaemonService { stats_period: u64, } @@ -85,49 +88,76 @@ impl DesktopDaemonService for DaemonService { ))? .into(); let ifname = &config.name; + let _span = info_span!("create_interface", interface_name = &ifname).entered(); info!("Creating interface {ifname}"); // setup WireGuard API let wgapi = setup_wgapi(ifname.clone())?; - // create new interface - debug!("Creating new interface {ifname}"); - wgapi.create_interface().map_err(|err| { - let msg = format!("Failed to create WireGuard interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; - - // configure interface - debug!("Configuring new interface {ifname} with configuration: {config:?}"); - wgapi.configure_interface(&config).map_err(|err| { - let msg = format!("Failed to configure WireGuard interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; + 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}"); + } - // configure routing - debug!("Configuring interface {ifname} routing"); - wgapi.configure_peer_routing(&config.peers).map_err(|err| { - let msg = - format!("Failed to configure routing for WireGuard interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; + #[cfg(not(windows))] + { + // create new interface + debug!("Creating new interface {ifname}"); + wgapi.create_interface().map_err(|err| { + let msg = format!("Failed to create WireGuard interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + } - // Configure dns - debug!("Configuring DNS for interface {ifname}"); let dns: Vec = request .dns .into_iter() .filter_map(|s| s.parse().ok()) .collect(); - wgapi.configure_dns(&dns).map_err(|err| { - let msg = format!("Failed to configure DNS for WireGuard interface {ifname}: {err}"); + // configure interface + debug!("Configuring new interface {ifname} with configuration: {config:?}"); + + #[cfg(not(windows))] + let configure_interface_result = wgapi.configure_interface(&config); + #[cfg(windows)] + let configure_interface_result = wgapi.configure_interface(&config, &dns); + + configure_interface_result.map_err(|err| { + let msg = format!("Failed to configure WireGuard interface {ifname}: {err}"); error!("{msg}"); Status::new(Code::Internal, msg) })?; + #[cfg(not(windows))] + { + // configure routing + debug!("Configuring interface {ifname} routing"); + wgapi.configure_peer_routing(&config.peers).map_err(|err| { + let msg = + format!("Failed to configure routing for WireGuard interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + + // Configure DNS + if !dns.is_empty() { + debug!("Configuring DNS for interface {ifname} with config: {dns:?}"); + wgapi.configure_dns(&dns).map_err(|err| { + let msg = + format!("Failed to configure DNS for WireGuard interface {ifname}: {err}"); + 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(())) } @@ -137,16 +167,26 @@ impl DesktopDaemonService for DaemonService { ) -> Result, Status> { let request = request.into_inner(); let ifname = request.interface_name; + let _span = info_span!("remove_interface", interface_name = &ifname).entered(); 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(())) } @@ -159,7 +199,10 @@ impl DesktopDaemonService for DaemonService { ) -> Result, Status> { let request = request.into_inner(); let ifname = request.interface_name; - info!("Starting interface data stream for {ifname}"); + let span = info_span!("read_interface_data", interface_name = &ifname); + span.in_scope(|| { + info!("Starting interface data stream for {ifname}"); + }); let stats_period = self.stats_period; let (tx, rx) = mpsc::channel(64); @@ -192,7 +235,7 @@ impl DesktopDaemonService for DaemonService { debug!("Finished sending stats update for interface {ifname}"); } warn!("Client disconnected from stats update stream for interface {ifname}"); - }); + }.instrument(span)); let output_stream = ReceiverStream::new(rx); Ok(Response::new( @@ -210,6 +253,7 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { info!("defguard daemon listening on {addr}"); Server::builder() + .trace_fn(|_| tracing::info_span!("defguard_service")) .add_service(DesktopDaemonServiceServer::new(daemon_service)) .serve(addr) .await?; diff --git a/src-tauri/src/service/utils.rs b/src-tauri/src/service/utils.rs index cac1c1f1..5aa7f25a 100644 --- a/src-tauri/src/service/utils.rs +++ b/src-tauri/src/service/utils.rs @@ -1,10 +1,21 @@ +use crate::{ + service::{ + proto::desktop_daemon_service_client::DesktopDaemonServiceClient, DaemonError, + DAEMON_BASE_URL, + }, + utils::get_service_log_dir, +}; +use std::io::stdout; use tonic::transport::channel::{Channel, Endpoint}; use tracing::debug; - -use crate::service::{ - proto::desktop_daemon_service_client::DesktopDaemonServiceClient, DaemonError, DAEMON_BASE_URL, +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{ + fmt, fmt::writer::MakeWriterExt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, + Layer, }; +use super::config::Config; + pub fn setup_client() -> Result, DaemonError> { debug!("Setting up gRPC client"); let endpoint = Endpoint::from_shared(DAEMON_BASE_URL)?; @@ -12,3 +23,35 @@ pub fn setup_client() -> Result, DaemonError let client = DesktopDaemonServiceClient::new(channel); Ok(client) } + +pub fn logging_setup(config: &Config) -> WorkerGuard { + // prepare log file appender + let log_dir = get_service_log_dir(); + let file_appender = tracing_appender::rolling::daily(log_dir, "defguard-service.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + // prepare log level filter for stdout + 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(stdout_filter); + let json_file_layer = fmt::layer() + .json() + .with_writer(non_blocking.with_max_level(tracing::Level::DEBUG)) + .with_filter(json_filter); + + // initialize tracing subscriber + tracing_subscriber::registry() + .with(stdout_layer) + .with(json_file_layer) + .init(); + + _guard +} diff --git a/src-tauri/src/service/windows_service.rs b/src-tauri/src/service/windows_service.rs new file mode 100644 index 00000000..29ec3592 --- /dev/null +++ b/src-tauri/src/service/windows_service.rs @@ -0,0 +1,120 @@ +#[cfg(windows)] +pub mod defguard_windows_service { + use crate::service::{run_server, utils::logging_setup, Config}; + use clap::Parser; + use log::error; + use std::{ffi::OsString, sync::mpsc, time::Duration}; + use tokio::runtime::Runtime; + use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, Result, + }; + + static SERVICE_NAME: &str = "DefguardService"; + const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + + pub fn run() -> Result<()> { + // Register generated `ffi_service_main` with the system and start the service, blocking + // this thread until the service is stopped. + service_dispatcher::start(SERVICE_NAME, ffi_service_main) + } + + define_windows_service!(ffi_service_main, service_main); + + pub fn service_main(_arguments: Vec) { + if let Err(err) = run_service() { + error!("Error while running the service. {err}"); + panic!("{err}"); + } + } + + fn run_service() -> Result<()> { + // Create a channel to be able to poll a stop event from the service worker loop. + let (shutdown_tx, shutdown_rx) = mpsc::channel::(); + let shutdown_tx_server = shutdown_tx.clone(); + + // Define system service event handler that will be receiving service events. + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + // Notifies a service to report its current status information to the service + // control manager. Always return NoError even if not implemented. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + + // Handle stop + ServiceControl::Stop => { + shutdown_tx.send(1).unwrap(); + ServiceControlHandlerResult::NoError + } + + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler. + // The returned status handle should be used to report service status changes to the system. + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; + + let rt = Runtime::new(); + + if let Ok(runtime) = rt { + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + let config: Config = Config::parse(); + let _guard = logging_setup(&config); + + let default_panic = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + default_panic(info); + std::process::exit(1); + })); + + runtime.spawn(async move { + let server_result = run_server(config).await; + + if server_result.is_err() { + shutdown_tx_server.send(2).unwrap(); + } + }); + + loop { + // Poll shutdown event. + match shutdown_rx.recv_timeout(Duration::from_secs(1)) { + // Break the loop either upon stop or channel disconnect + Ok(1) | Err(mpsc::RecvTimeoutError::Disconnected) => break, + Ok(2) => { + panic!("Server has stopped working.") + } + Ok(_) => break, + + // Continue work if no events were received within the timeout + Err(mpsc::RecvTimeoutError::Timeout) => (), + }; + } + + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + } + + Ok(()) + } +} diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 682b1b04..8a702aed 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -1,13 +1,114 @@ -use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem}; +use tauri::{ + AppHandle, CustomMenuItem, Manager, State, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, +}; + +use crate::{appstate::AppState, database::TrayIconTheme, error::Error}; + +static SUBSCRIBE_UPDATES_LINK: &str = "https://defguard.net/newsletter"; +static JOIN_COMMUNITY_LINK: &str = "https://matrix.to/#/#defguard:teonite.com"; +static FOLLOW_US_LINK: &str = "https://floss.social/@defguard"; #[must_use] pub fn create_tray_menu() -> SystemTrayMenu { let quit = CustomMenuItem::new("quit".to_string(), "Quit"); let show = CustomMenuItem::new("show".to_string(), "Show"); let hide = CustomMenuItem::new("hide".to_string(), "Hide"); + let subscribe_updates = + CustomMenuItem::new("subscribe_updates".to_string(), "Subscribe for updates"); + let join_community = CustomMenuItem::new("join_community".to_string(), "Join our Community"); + let follow_us = CustomMenuItem::new("follow_us".to_string(), "Follow us"); SystemTrayMenu::new() .add_item(show) .add_item(hide) .add_native_item(SystemTrayMenuItem::Separator) + .add_item(subscribe_updates) + .add_item(join_community) + .add_item(follow_us) + .add_native_item(SystemTrayMenuItem::Separator) .add_item(quit) } + +fn show_main_window(app: &AppHandle) { + if let Some(main_window) = app.get_window("main") { + // if this fails tauri has a problem + let minimized = main_window.is_minimizable().unwrap(); + let visible = main_window.is_visible().unwrap(); + if minimized { + main_window.unminimize().unwrap(); + main_window.set_focus().unwrap(); + } + if !visible { + main_window.show().unwrap(); + main_window.set_focus().unwrap(); + } + } +} + +// handle tray actions +pub fn handle_tray_event(app: &AppHandle, event: SystemTrayEvent) { + match event { + SystemTrayEvent::LeftClick { .. } => { + if let Some(main_window) = app.get_window("main") { + let visibility = main_window.is_visible().unwrap(); + if visibility { + main_window.hide().unwrap(); + } else { + show_main_window(app); + } + } + } + SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { + "quit" => { + let app_state: State = app.state(); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let _ = app_state.close_all_connections().await; + app.exit(0); + }); + }); + } + "show" => show_main_window(app), + "hide" => { + if let Some(main_window) = app.get_window("main") { + if main_window + .is_visible() + .expect("Failed to check main window visibility") + { + main_window.hide().expect("Failed to hide main window"); + } + } + } + "subscribe_updates" => { + let _ = webbrowser::open(SUBSCRIBE_UPDATES_LINK); + } + "join_community" => { + let _ = webbrowser::open(JOIN_COMMUNITY_LINK); + } + "follow_us" => { + let _ = webbrowser::open(FOLLOW_US_LINK); + } + _ => {} + }, + _ => {} + } +} + +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/src/utils.rs b/src-tauri/src/utils.rs index 8146f6e0..1af569da 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,21 +1,35 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, + path::PathBuf, + process::Command, 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, Connection, DbPool, Location, Tunnel, TunnelConnection, 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, RemoveInterfaceRequest, + }, }, + 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; @@ -25,6 +39,7 @@ pub static DEFAULT_ROUTE: &str = "0.0.0.0/0"; pub async fn setup_interface( location: &Location, interface_name: String, + preshared_key: Option, pool: &DbPool, mut client: DesktopDaemonServiceClient, ) -> Result<(), Error> { @@ -39,6 +54,11 @@ pub async fn setup_interface( peer.endpoint = Some(endpoint); peer.persistent_keepalive_interval = Some(25); + if let Some(psk) = preshared_key { + let peer_psk = Key::from_str(&psk)?; + peer.preshared_key = Some(peer_psk); + } + debug!("Parsing location allowed ips: {}", location.allowed_ips); let allowed_ips: Vec = if location.route_all_traffic { debug!("Using all traffic routing: {DEFAULT_ROUTE}"); @@ -79,6 +99,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}"); @@ -143,8 +165,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 { @@ -158,7 +180,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(); @@ -178,12 +204,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, &state.get_pool()) + 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 location stats: {location_stats:#?}"); - let _ = location_stats.save(&state.get_pool()).await; - debug!("Saved location stats: {location_stats:#?}"); + debug!("Saving tunnel stats: {tunnel_stats:#?}"); + let _ = tunnel_stats.save(&state.get_pool()).await; + debug!("Saved location stats: {tunnel_stats:#?}"); + } } } Err(err) => { @@ -212,3 +255,413 @@ pub fn load_log_targets() -> Vec { Err(_) => Vec::new(), } } + +// helper function to get log file directory for the defguard-service daemon +pub fn get_service_log_dir() -> PathBuf { + #[cfg(target_os = "windows")] + let path = PathBuf::from("/Logs/defguard-service"); + + #[cfg(not(target_os = "windows"))] + let path = PathBuf::from("/var/log/defguard-service"); + + 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.trim()) { + 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(), + 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}"); + 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, + preshared_key: Option, + 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(), + preshared_key, + &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(()) +} +/// 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(()) +} diff --git a/src-tauri/src/wg_config.rs b/src-tauri/src/wg_config.rs new file mode 100644 index 00000000..6937e58c --- /dev/null +++ b/src-tauri/src/wg_config.rs @@ -0,0 +1,154 @@ +use crate::database::Tunnel; +use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; +use std::{array::TryFromSliceError, net::IpAddr}; +use thiserror::Error; +use x25519_dalek::{PublicKey, StaticSecret}; + +#[derive(Debug, Error)] +pub enum WireguardConfigParseError { + #[error(transparent)] + ParseError(#[from] ini::ParseError), + #[error("Config section not found: {0}")] + SectionNotFound(String), + #[error("Config key not found: {0}")] + KeyNotFound(String), + #[error("Invalid peer IP: {0}")] + InvalidPeerIp(IpAddr), + #[error("Invalid key: {0}")] + InvalidKey(String), + #[error("Invalid port: {0}")] + InvalidPort(String), +} + +impl From for WireguardConfigParseError { + fn from(e: TryFromSliceError) -> Self { + WireguardConfigParseError::InvalidKey(format!("{e}")) + } +} + +impl From for WireguardConfigParseError { + fn from(e: DecodeError) -> Self { + WireguardConfigParseError::InvalidKey(format!("{e}")) + } +} + +pub fn parse_wireguard_config(config: &str) -> Result { + let config = ini::Ini::load_from_str(config)?; + + // Parse Interface section + let interface_section = config + .section(Some("Interface")) + .ok_or_else(|| WireguardConfigParseError::SectionNotFound("Interface".to_string()))?; + let prvkey = interface_section + .get("PrivateKey") + .ok_or_else(|| WireguardConfigParseError::KeyNotFound("PrivateKey".to_string()))?; + let prvkey_bytes: [u8; 32] = BASE64_STANDARD + .decode(prvkey.as_bytes())? + .try_into() + .map_err(|_| WireguardConfigParseError::InvalidKey(prvkey.to_string()))?; + let pubkey = + BASE64_STANDARD.encode(PublicKey::from(&StaticSecret::from(prvkey_bytes)).to_bytes()); + let address = interface_section + .get("Address") + .ok_or_else(|| WireguardConfigParseError::KeyNotFound("Address".to_string()))?; + // extract IP if DNS config includes search domains + // FIXME: actually handle search domains + let dns = interface_section + .get("DNS") + .map(|dns| match dns.split(',').next() { + Some(address) => address.to_string(), + None => dns.to_string(), + }); + + let pre_up = interface_section.get("PreUp"); + let post_up = interface_section.get("PostUp"); + let pre_down = interface_section.get("PreDown"); + let post_down = interface_section.get("PostDown"); + + // Parse Peer section (assuming only one peer) + let peer_section = config + .section(Some("Peer")) + .ok_or_else(|| WireguardConfigParseError::SectionNotFound("Peer".to_string()))?; + + // Extract additional fields from the Peer section + let peer_pubkey = peer_section + .get("PublicKey") + .ok_or_else(|| WireguardConfigParseError::KeyNotFound("PublicKey".to_string()))?; + let peer_allowed_ips = peer_section.get("AllowedIPs"); + + let endpoint = peer_section + .get("Endpoint") + .ok_or_else(|| WireguardConfigParseError::KeyNotFound("Endpoint".to_string()))?; + let persistent_keep_alive = peer_section + .get("PersistentKeepalive") + .unwrap_or("25") + .parse() + .unwrap(); + + // Create or modify the Tunnel struct with the parsed values using the `new` method + let tunnel = Tunnel::new( + "".into(), + pubkey, + prvkey.into(), + address.into(), + peer_pubkey.into(), + peer_allowed_ips.map(str::to_string), + endpoint.into(), + dns, + persistent_keep_alive, + false, // Adjust as needed + pre_up.map(str::to_string), + post_up.map(str::to_string), + pre_down.map(str::to_string), + post_down.map(str::to_string), + ); + + Ok(tunnel) +} +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_config() { + let config = " + [Interface] + PrivateKey = GAA2X3DW0WakGVx+DsGjhDpTgg50s1MlmrLf24Psrlg= + Address = 10.0.0.1/24 + ListenPort = 55055 + 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] + PublicKey = BvUB3iZq3U0jZrY6b4KbGhz0IVZzpAdbJiRZGdci9ZU= + AllowedIPs = 10.0.0.10/24, 10.2.0.1/24, 0.0.0.0/0 + Endpoint = 10.0.0.0:1234 + PersistentKeepalive = 300 + + + "; + let tunnel = parse_wireguard_config(config).unwrap(); + assert_eq!( + tunnel.prvkey, + "GAA2X3DW0WakGVx+DsGjhDpTgg50s1MlmrLf24Psrlg=" + ); + assert_eq!(tunnel.id, None); + assert_eq!(tunnel.name, ""); + assert_eq!(tunnel.address, "10.0.0.1/24"); + assert_eq!( + tunnel.server_pubkey, + "BvUB3iZq3U0jZrY6b4KbGhz0IVZzpAdbJiRZGdci9ZU=" + ); + assert_eq!(tunnel.endpoint, "10.0.0.0:1234"); + assert_eq!(tunnel.dns, Some("10.0.0.2".to_string())); + assert_eq!( + tunnel.allowed_ips, + Some("10.0.0.10/24, 10.2.0.1/24, 0.0.0.0/0".into()) + ); + assert_eq!(tunnel.pre_up, None); + assert_eq!(tunnel.post_up, + Some("iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT".to_string())); + assert_eq!(tunnel.pre_down, None); + assert_eq!(tunnel.post_down, None); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index aac6d400..22e25064 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,14 +8,14 @@ }, "package": { "productName": "defguard-client", - "version": "0.1.1" + "version": "0.2.0" }, "tauri": { "systemTray": { - "iconPath": "icons/32x32.png", - "iconAsTemplate": false + "iconPath": "resources/icons/tray-32x32-color.png", + "iconAsTemplate": false, + "menuOnLeftClick": false }, - "allowlist": { "all": false, "window": { @@ -25,6 +25,16 @@ "all": true, "request": true, "scope": ["https://**", "http://**"] + }, + "fs": { + "all": true, + "scope": ["$RESOURCE/*", "$APPDATA/*"] + }, + "clipboard": { + "all": true + }, + "dialog": { + "all": true } }, "bundle": { @@ -58,13 +68,20 @@ "providerShortName": null, "signingIdentity": null }, - "resources": [], + "resources": [ + "resources/*" + ], "shortDescription": "", "targets": ["deb", "app", "appimage"], "windows": { "certificateThumbprint": null, "digestAlgorithm": "sha256", - "timestampUrl": "" + "timestampUrl": "", + "wix": { + "template": "./resources-windows/main.wxs", + "fragmentPaths": ["./resources-windows/service-fragment.wxs"], + "componentRefs": ["DefGuardServiceFragment"] + } } }, "security": { diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index 6ce3eb3f..22f1d767 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -5,8 +5,9 @@ "resources-macos/binaries/*" ], "resources": [ - "resources-macos/resources/*" + "resources-macos/resources/*", + "resources/*" ] } } -} \ No newline at end of file +} diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json new file mode 100644 index 00000000..8bd799eb --- /dev/null +++ b/src-tauri/tauri.windows.conf.json @@ -0,0 +1,11 @@ +{ + "tauri": { + "bundle": { + "targets": ["msi"], + "resources": [ + "resources-windows/binaries/*", + "resources/*" + ] + } + } +} diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index e7ce4522..3223a416 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,13 +20,23 @@ 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 { 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'; +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'; +import { ThemeProvider } from '../../shared/providers/ThemeProvider/ThemeProvider'; import { routes } from '../../shared/routes'; +import { ApplicationUpdateManager } from '../ApplicationUpdateManager/ApplicationUpdateManager'; dayjs.extend(duration); dayjs.extend(utc); @@ -38,6 +48,8 @@ dayjs.extend(timezone); const queryClient = new QueryClient(); +const { getSettings, getInstances, getTunnels } = clientApi; + const router = createBrowserRouter([ { index: true, @@ -58,12 +70,40 @@ const router = createBrowserRouter([ { path: '/client/', index: true, + element: , + }, + { + path: '/client/instance', element: , }, + { + path: '/client/carousel', + element: , + }, { 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: , + }, + { + path: '/client/settings', + element: , + }, { path: '/client/*', element: , @@ -79,8 +119,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,14 +138,31 @@ export const App = () => { dayjs.locale(detectedLocale); }, []); - if (!wasLoaded) return null; + // load settings from tauri first time + useEffect(() => { + const loadTauriState = async () => { + debug('App init state from tauri'); + const settings = await getSettings(); + const instances = await getInstances(); + const tunnels = await getTunnels(); + setClientState({ settings, instances, tunnels }); + debug('Tauri init data loaded'); + setSettingsLoaded(true); + }; + loadTauriState(); + }, [setClientState, setSettingsLoaded]); + + if (!appLoaded) return null; return ( - + + + + ); }; diff --git a/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx b/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx new file mode 100644 index 00000000..188eb7a5 --- /dev/null +++ b/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx @@ -0,0 +1,86 @@ +import { getVersion } from '@tauri-apps/api/app'; +import { listen, UnlistenFn } from '@tauri-apps/api/event'; +import { useEffect, useState } from 'react'; + +import { clientApi } from '../../pages/client/clientAPI/clientApi.ts'; +import { useClientStore } from '../../pages/client/hooks/useClientStore'; +import { TauriEventKey } from '../../pages/client/types'; +import { NewApplicationVersionInfo } from '../../shared/hooks/api/types'; +import { + ApplicationUpdateStore, + useApplicationUpdateStore, +} from './useApplicationUpdateStore'; + +const { getLatestAppVersion } = clientApi; + +export const ApplicationUpdateManager = () => { + const [appVersion, setAppVersion] = useState(undefined); + + const setApplicationUpdateData = useApplicationUpdateStore((state) => state.setValues); + const checkForUpdates = useClientStore((state) => state.settings.check_for_updates); + + // Get current application version. + useEffect(() => { + const getAppVersion = async () => { + const version = await getVersion().catch(() => { + return ''; + }); + setAppVersion(version); + }; + + getAppVersion(); + }, []); + + // Listen to new application release info. + useEffect(() => { + const subs: UnlistenFn[] = []; + + // Stop listening if "check for updates" setting has been turned off. + if (!checkForUpdates) { + subs.forEach((sub) => sub()); + return; + } + + listen(TauriEventKey.APP_VERSION_FETCH, (data) => { + const payload = data.payload as NewApplicationVersionInfo; + const state = { + latestVersion: payload.version, + releaseDate: payload.release_date, + releaseNotesUrl: payload.release_notes_url, + updateUrl: payload.update_url, + dismissed: false, + } as ApplicationUpdateStore; + setApplicationUpdateData(state); + }).then((cleanup) => { + subs.push(cleanup); + }); + + return () => { + subs.forEach((sub) => sub()); + }; + }, [checkForUpdates, setApplicationUpdateData]); + + // Check for updates on launch and when "check for updates" setting has been turned on. + useEffect(() => { + if (!checkForUpdates || !appVersion) return; + + const getNewVersion = async (appVersion: string) => { + if (!appVersion) return; + + const response = await getLatestAppVersion(); + + setApplicationUpdateData({ + currentVersion: appVersion, + latestVersion: response.version, + releaseDate: response.release_date, + releaseNotesUrl: response.release_notes_url, + updateUrl: response.update_url, + dismissed: false, + }); + }; + + getNewVersion(appVersion); + }, [checkForUpdates, appVersion, setApplicationUpdateData]); + + return null; +}; diff --git a/src/components/ApplicationUpdateManager/useApplicationUpdateStore.tsx b/src/components/ApplicationUpdateManager/useApplicationUpdateStore.tsx new file mode 100644 index 00000000..4adbd30a --- /dev/null +++ b/src/components/ApplicationUpdateManager/useApplicationUpdateStore.tsx @@ -0,0 +1,28 @@ +import { createWithEqualityFn } from 'zustand/traditional'; + +export interface ApplicationUpdateStore { + currentVersion: string | undefined; + latestVersion: string | undefined; + releaseDate: string | undefined; + releaseNotesUrl: string | undefined; + updateUrl: string | undefined; + dismissed: boolean; + setValues: (values: Partial) => void; +} + +const defaultState = { + currentVersion: undefined, + latestVersion: undefined, + releaseDate: undefined, + releaseNotesUrl: undefined, + updateUrl: undefined, + dismissed: false, +} as ApplicationUpdateStore; + +export const useApplicationUpdateStore = createWithEqualityFn( + (set) => ({ + ...defaultState, + setValues: (values: Partial) => set({ ...values }), + }), + Object.is, +); diff --git a/src/components/ApplicationUpdateManager/useNewAppVersionAvailable.tsx b/src/components/ApplicationUpdateManager/useNewAppVersionAvailable.tsx new file mode 100644 index 00000000..74764914 --- /dev/null +++ b/src/components/ApplicationUpdateManager/useNewAppVersionAvailable.tsx @@ -0,0 +1,15 @@ +import { compareVersions } from 'compare-versions'; + +import { useApplicationUpdateStore } from './useApplicationUpdateStore'; + +export const useNewAppVersionAvailable = () => { + const newAppVersionAvailable = useApplicationUpdateStore((state) => { + if (!state.currentVersion || !state.latestVersion) return false; + + return compareVersions(state.latestVersion, state.currentVersion) === 1; + }); + + return { + newAppVersionAvailable, + }; +}; diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index dd8cb6ed..7d98d7e1 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: { @@ -52,6 +52,121 @@ const en = { pages: { client: { pages: { + carouselPage: { + slides: { + shared: { + //md + 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.', + }, + tunnel: { + title: 'Add Tunnel', + 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: `, + githubText: `staring us on`, + githubLink: `GitHub`, + spreadWordText: `spreading the word about:`, + defguard: `defguard!`, + joinMatrix: `join our Matrix server:`, + supportUs: 'Support Us!', + }, + }, + }, + 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', + }, + }, + versionUpdate: { + title: 'Updates', + checkboxTitle: 'Check for updates', + }, + }, + }, + }, + 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: { @@ -70,6 +185,7 @@ const en = { }, header: { title: 'Locations', + edit: 'Edit Instance', filters: { views: { grid: 'Grid View', @@ -106,6 +222,141 @@ const en = { download: 'Download', }, }, + details: { + title: 'Details', + logs: { + title: 'Log', + }, + info: { + configuration: { + title: 'Device configuration', + pubkey: 'Public key', + address: 'Addresses', + listenPort: 'Listen port', + }, + vpn: { + title: 'VPN Server Configuration', + pubkey: 'Public key', + serverAddress: 'Server Address', + allowedIps: 'Allowed IPs', + dns: 'DNS servers', + keepalive: 'Persistent keepalive', + handshake: 'Latest Handshake', + handshakeValue: '{seconds: number} seconds ago', + }, + }, + }, + }, + }, + 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', + forms: { + initTunnel: { + title: 'Please provide Instance URL and token', + sections: { + vpnServer: 'VPN Server', + advancedOptions: 'Advanced Options', + }, + labels: { + name: 'Tunnel Name', + privateKey: 'Private Key', + publicKey: 'Public Key', + address: 'Address', + serverPubkey: 'Public Key', + endpoint: 'VPN Server Address:Port', + dns: 'DNS', + allowedips: 'Allowed IPs (separate with comma)', + persistentKeepAlive: 'Persistent Keep Alive (sec)', + preUp: 'PreUp', + postUp: 'PostUp', + PreDown: 'PreDown', + PostDown: 'PostDown', + }, + helpers: { + advancedOptions: + 'Click the "Advanced Options" section to reveal additional settings for fine-tuning your WireGuard tunnel configuration. You can customize pre and post scripts, among other options.', + name: 'A unique name for your WireGuard tunnel to identify it easily.', + pubkey: + 'The public key associated with the WireGuard tunnel for secure communication.', + prvkey: + 'The private key associated with the WireGuard tunnel for secure communication.', + address: + 'The IP address assigned to this WireGuard client within the VPN network.', + serverPubkey: + 'The public key of the WireGuard server for secure communication.', + allowedIps: + '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. 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: + 'If enabled, all network traffic will be routed through the WireGuard tunnel.', + preUp: + 'Shell commands or scripts to be executed before bringing up the WireGuard tunnel.', + postUp: + 'Shell commands or scripts to be executed after bringing up the WireGuard tunnel.', + preDown: + 'Shell commands or scripts to be executed before tearing down the WireGuard tunnel.', + postDown: + 'Shell commands or scripts to be executed after tearing down the WireGuard tunnel.', + }, + submit: 'Add Tunnel', + messages: { + configError: 'Error parsing config file', + addSuccess: 'Tunnel added', + addError: 'Creating tunnel failed', + }, + controls: { + importConfig: 'Import Config File', + generatePrvkey: 'Generate Private Key', + }, + }, + }, + guide: { + title: 'Adding WireGuard tunnel', + subTitle: `

To establish secure communication between two or more devices over the internet create a virtual private network by configuring your tunnel.

If you don’t see options like Table or MTU it means we do not support it for now, but will be added later.

`, + card: { + title: 'Setting Up A new Tunnel:', + content: ` +

1. Import Configuration File

+
+
    +
  • Click on the "Import Config File" button.
  • +
  • Navigate to configuration file using the file selection dialog.
  • +
  • Select the .conf file you received or created.
  • +
+
+

2. Or Fill in Form on the Left

+
+
    +
  • Enter a name for the tunnel.
  • +
  • Provide essential details such as the private key, public key, and endpoint (server address).
  • +
+
+

+ For more help, please visit defguard help (https://defguard.gitbook.io/) +

+ `, + }, }, }, addInstancePage: { @@ -158,12 +409,21 @@ const en = { }, }, sideBar: { - instances: 'Instances', + instances: 'defguard Instances', addInstance: 'Add Instance', + addTunnel: 'Add Tunnel', + tunnels: 'WireGuard Tunnels', + settings: 'Settings', copyright: { copyright: `Copyright © 2023`, appVersion: 'Application version: {version:string}', }, + applicationVersion: 'Application version: ', + }, + newApplicationVersion: { + header: 'New version available', + dismiss: 'Dismiss', + releaseNotes: "See what's new", }, }, enrollment: { @@ -191,7 +451,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.`, @@ -251,7 +511,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: { @@ -279,7 +539,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.

`, }, @@ -298,7 +558,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.', @@ -354,6 +614,82 @@ If you want to disengage your VPN connection, simply press "deactivate". }, }, }, + modals: { + updateInstance: { + title: 'Update instance', + infoMessage: + "Enter the token sent by the administrator to update the Instance configuration.\nAlternatively, you can choose to remove this Instance entirely by clicking the 'Remove Instance' button below.", + form: { + fieldLabels: { + token: 'Token', + url: 'URL', + }, + fieldErrors: { + token: { + rejected: 'Token or URL rejected.', + instanceIsNotPresent: 'Instance for this token was not found.', + }, + }, + }, + controls: { + updateInstance: 'Update Instance', + removeInstance: 'Remove Instance', + }, + messages: { + success: '{name: string} updated.', + error: 'Token or URL is invalid.', + errorInstanceNotFound: 'Instance for given token is not registered !', + }, + }, + deleteInstance: { + title: 'Delete instance', + subtitle: 'Are you sure you want to delete {name: string}?', + messages: { + success: 'Instance deleted', + error: 'Unexpected error occurred', + }, + controls: { + submit: 'Delete instance', + }, + }, + deleteTunnel: { + title: 'Delete tunnel', + subtitle: 'Are you sure you want to delete {name: string}?', + messages: { + success: 'Tunnel deleted', + error: 'Unexpected error occurred', + }, + controls: { + submit: 'Delete tunnel', + }, + }, + mfa: { + authentication: { + title: 'Two-factor authentication', + authenticatorAppDescription: + 'Paste the authentication code from your Authenticator Application.', + emailCodeDescription: + 'Paste the authentication code that was sent to your email address.', + mfaStartDescriptionPrimary: + 'For this connection, two-factor authentication (2FA) is mandatory.', + mfaStartDescriptionSecondary: 'Select your preferred authentication method.', + useAuthenticatorApp: 'Use authenticator app', + useEmailCode: 'Use your email code', + saveAuthenticationMethodForFutureLogins: 'Use this method for future logins', + buttonSubmit: 'Verify', + errors: { + mfaNotConfigured: 'Selected method has not been configured.', + mfaStartGeneric: + 'Could not start MFA process. Please try again or contact administrator.', + instanceNotFound: 'Could not find instance.', + locationNotSpecified: 'Location is not specified.', + invalidCode: + 'Error, this code is invalid, try again or contact your administrator.', + tokenExpired: 'Token has expired. Please try to connect again.', + }, + }, + }, + }, } satisfies BaseTranslation; export default en; diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 8d83bdcc..47c60778 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: { @@ -136,6 +136,239 @@ 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 + } + tunnel: { + /** + * A​d​d​ ​T​u​n​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​:​ + */ + 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 + /** + * S​u​p​p​o​r​t​ ​U​s​! + */ + supportUs: string + } + } + } + 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 + } + } + versionUpdate: { + /** + * U​p​d​a​t​e​s + */ + title: string + /** + * C​h​e​c​k​ ​f​o​r​ ​u​p​d​a​t​e​s + */ + checkboxTitle: string + } + } + } + } + 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 @@ -164,7 +397,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 } @@ -174,6 +411,10 @@ type RootTranslation = { * L​o​c​a​t​i​o​n​s */ title: string + /** + * E​d​i​t​ ​I​n​s​t​a​n​c​e + */ + edit: string filters: { views: { /** @@ -262,6 +503,314 @@ type RootTranslation = { download: string } } + details: { + /** + * D​e​t​a​i​l​s + */ + title: string + logs: { + /** + * L​o​g + */ + title: string + } + info: { + configuration: { + /** + * D​e​v​i​c​e​ ​c​o​n​f​i​g​u​r​a​t​i​o​n + */ + title: string + /** + * P​u​b​l​i​c​ ​k​e​y + */ + pubkey: string + /** + * A​d​d​r​e​s​s​e​s + */ + address: string + /** + * L​i​s​t​e​n​ ​p​o​r​t + */ + listenPort: string + } + vpn: { + /** + * V​P​N​ ​S​e​r​v​e​r​ ​C​o​n​f​i​g​u​r​a​t​i​o​n + */ + title: string + /** + * P​u​b​l​i​c​ ​k​e​y + */ + pubkey: string + /** + * S​e​r​v​e​r​ ​A​d​d​r​e​s​s + */ + serverAddress: string + /** + * A​l​l​o​w​e​d​ ​I​P​s + */ + allowedIps: string + /** + * D​N​S​ ​s​e​r​v​e​r​s + */ + dns: string + /** + * P​e​r​s​i​s​t​e​n​t​ ​k​e​e​p​a​l​i​v​e + */ + keepalive: string + /** + * L​a​t​e​s​t​ ​H​a​n​d​s​h​a​k​e + */ + handshake: string + /** + * {​s​e​c​o​n​d​s​}​ ​s​e​c​o​n​d​s​ ​a​g​o + * @param {number} seconds + */ + handshakeValue: RequiredParams<'seconds'> + } + } + } + } + } + tunnelPage: { + /** + * 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: { + /** + * A​d​d​ ​W​i​r​e​G​u​a​r​d​®​ ​T​u​n​n​e​l + */ + title: string + forms: { + initTunnel: { + /** + * P​l​e​a​s​e​ ​p​r​o​v​i​d​e​ ​I​n​s​t​a​n​c​e​ ​U​R​L​ ​a​n​d​ ​t​o​k​e​n + */ + title: string + sections: { + /** + * V​P​N​ ​S​e​r​v​e​r + */ + vpnServer: string + /** + * A​d​v​a​n​c​e​d​ ​O​p​t​i​o​n​s + */ + advancedOptions: string + } + labels: { + /** + * T​u​n​n​e​l​ ​N​a​m​e + */ + name: string + /** + * P​r​i​v​a​t​e​ ​K​e​y + */ + privateKey: string + /** + * P​u​b​l​i​c​ ​K​e​y + */ + publicKey: string + /** + * A​d​d​r​e​s​s + */ + address: string + /** + * P​u​b​l​i​c​ ​K​e​y + */ + serverPubkey: string + /** + * V​P​N​ ​S​e​r​v​e​r​ ​A​d​d​r​e​s​s​:​P​o​r​t + */ + endpoint: string + /** + * D​N​S + */ + dns: string + /** + * 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 + /** + * P​e​r​s​i​s​t​e​n​t​ ​K​e​e​p​ ​A​l​i​v​e​ ​(​s​e​c​) + */ + persistentKeepAlive: string + /** + * P​r​e​U​p + */ + preUp: string + /** + * P​o​s​t​U​p + */ + postUp: string + /** + * P​r​e​D​o​w​n + */ + PreDown: string + /** + * P​o​s​t​D​o​w​n + */ + PostDown: string + } + helpers: { + /** + * C​l​i​c​k​ ​t​h​e​ ​"​A​d​v​a​n​c​e​d​ ​O​p​t​i​o​n​s​"​ ​s​e​c​t​i​o​n​ ​t​o​ ​r​e​v​e​a​l​ ​a​d​d​i​t​i​o​n​a​l​ ​s​e​t​t​i​n​g​s​ ​f​o​r​ ​f​i​n​e​-​t​u​n​i​n​g​ ​y​o​u​r​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​.​ ​Y​o​u​ ​c​a​n​ ​c​u​s​t​o​m​i​z​e​ ​p​r​e​ ​a​n​d​ ​p​o​s​t​ ​s​c​r​i​p​t​s​,​ ​a​m​o​n​g​ ​o​t​h​e​r​ ​o​p​t​i​o​n​s​. + */ + advancedOptions: string + /** + * A​ ​u​n​i​q​u​e​ ​n​a​m​e​ ​f​o​r​ ​y​o​u​r​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​ ​t​o​ ​i​d​e​n​t​i​f​y​ ​i​t​ ​e​a​s​i​l​y​. + */ + name: string + /** + * T​h​e​ ​p​u​b​l​i​c​ ​k​e​y​ ​a​s​s​o​c​i​a​t​e​d​ ​w​i​t​h​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​ ​f​o​r​ ​s​e​c​u​r​e​ ​c​o​m​m​u​n​i​c​a​t​i​o​n​. + */ + pubkey: string + /** + * T​h​e​ ​p​r​i​v​a​t​e​ ​k​e​y​ ​a​s​s​o​c​i​a​t​e​d​ ​w​i​t​h​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​ ​f​o​r​ ​s​e​c​u​r​e​ ​c​o​m​m​u​n​i​c​a​t​i​o​n​. + */ + prvkey: string + /** + * T​h​e​ ​I​P​ ​a​d​d​r​e​s​s​ ​a​s​s​i​g​n​e​d​ ​t​o​ ​t​h​i​s​ ​W​i​r​e​G​u​a​r​d​ ​c​l​i​e​n​t​ ​w​i​t​h​i​n​ ​t​h​e​ ​V​P​N​ ​n​e​t​w​o​r​k​. + */ + address: string + /** + * T​h​e​ ​p​u​b​l​i​c​ ​k​e​y​ ​o​f​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​s​e​r​v​e​r​ ​f​o​r​ ​s​e​c​u​r​e​ ​c​o​m​m​u​n​i​c​a​t​i​o​n​. + */ + serverPubkey: string + /** + * A​ ​c​o​m​m​a​-​s​e​p​a​r​a​t​e​d​ ​l​i​s​t​ ​o​f​ ​I​P​ ​a​d​d​r​e​s​s​e​s​ ​o​r​ ​C​I​D​R​ ​r​a​n​g​e​s​ ​t​h​a​t​ ​a​r​e​ ​a​l​l​o​w​e​d​ ​f​o​r​ ​c​o​m​m​u​n​i​c​a​t​i​o​n​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​t​u​n​n​e​l​. + */ + allowedIps: string + /** + * T​h​e​ ​a​d​d​r​e​s​s​ ​a​n​d​ ​p​o​r​t​ ​o​f​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​s​e​r​v​e​r​,​ ​t​y​p​i​c​a​l​l​y​ ​i​n​ ​t​h​e​ ​f​o​r​m​a​t​ ​"​h​o​s​t​n​a​m​e​:​p​o​r​t​"​. + */ + 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​.​ ​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 + /** + * T​h​e​ ​i​n​t​e​r​v​a​l​ ​(​i​n​ ​s​e​c​o​n​d​s​)​ ​f​o​r​ ​s​e​n​d​i​n​g​ ​p​e​r​i​o​d​i​c​ ​k​e​e​p​-​a​l​i​v​e​ ​m​e​s​s​a​g​e​s​ ​t​o​ ​e​n​s​u​r​e​ ​t​h​e​ ​t​u​n​n​e​l​ ​s​t​a​y​s​ ​a​c​t​i​v​e​.​ ​A​d​j​u​s​t​ ​a​s​ ​n​e​e​d​e​d​. + */ + persistentKeepAlive: string + /** + * I​f​ ​e​n​a​b​l​e​d​,​ ​a​l​l​ ​n​e​t​w​o​r​k​ ​t​r​a​f​f​i​c​ ​w​i​l​l​ ​b​e​ ​r​o​u​t​e​d​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​. + */ + routeAllTraffic: string + /** + * S​h​e​l​l​ ​c​o​m​m​a​n​d​s​ ​o​r​ ​s​c​r​i​p​t​s​ ​t​o​ ​b​e​ ​e​x​e​c​u​t​e​d​ ​b​e​f​o​r​e​ ​b​r​i​n​g​i​n​g​ ​u​p​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​. + */ + preUp: string + /** + * S​h​e​l​l​ ​c​o​m​m​a​n​d​s​ ​o​r​ ​s​c​r​i​p​t​s​ ​t​o​ ​b​e​ ​e​x​e​c​u​t​e​d​ ​a​f​t​e​r​ ​b​r​i​n​g​i​n​g​ ​u​p​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​. + */ + postUp: string + /** + * S​h​e​l​l​ ​c​o​m​m​a​n​d​s​ ​o​r​ ​s​c​r​i​p​t​s​ ​t​o​ ​b​e​ ​e​x​e​c​u​t​e​d​ ​b​e​f​o​r​e​ ​t​e​a​r​i​n​g​ ​d​o​w​n​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​. + */ + preDown: string + /** + * S​h​e​l​l​ ​c​o​m​m​a​n​d​s​ ​o​r​ ​s​c​r​i​p​t​s​ ​t​o​ ​b​e​ ​e​x​e​c​u​t​e​d​ ​a​f​t​e​r​ ​t​e​a​r​i​n​g​ ​d​o​w​n​ ​t​h​e​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l​. + */ + postDown: string + } + /** + * A​d​d​ ​T​u​n​n​e​l + */ + submit: string + messages: { + /** + * E​r​r​o​r​ ​p​a​r​s​i​n​g​ ​c​o​n​f​i​g​ ​f​i​l​e + */ + configError: string + /** + * T​u​n​n​e​l​ ​a​d​d​e​d + */ + addSuccess: string + /** + * C​r​e​a​t​i​n​g​ ​t​u​n​n​e​l​ ​f​a​i​l​e​d + */ + addError: string + } + controls: { + /** + * I​m​p​o​r​t​ ​C​o​n​f​i​g​ ​F​i​l​e + */ + importConfig: string + /** + * G​e​n​e​r​a​t​e​ ​P​r​i​v​a​t​e​ ​K​e​y + */ + generatePrvkey: string + } + } + } + guide: { + /** + * A​d​d​i​n​g​ ​W​i​r​e​G​u​a​r​d​ ​t​u​n​n​e​l + */ + title: string + /** + * <​p​>​T​o​ ​e​s​t​a​b​l​i​s​h​ ​s​e​c​u​r​e​ ​c​o​m​m​u​n​i​c​a​t​i​o​n​ ​b​e​t​w​e​e​n​ ​t​w​o​ ​o​r​ ​m​o​r​e​ ​d​e​v​i​c​e​s​ ​o​v​e​r​ ​t​h​e​ ​i​n​t​e​r​n​e​t​ ​c​r​e​a​t​e​ ​a​ ​v​i​r​t​u​a​l​ ​p​r​i​v​a​t​e​ ​n​e​t​w​o​r​k​ ​b​y​ ​c​o​n​f​i​g​u​r​i​n​g​ ​y​o​u​r​ ​t​u​n​n​e​l​.​<​/​p​>​<​p​>​I​f​ ​y​o​u​ ​d​o​n​’​t​ ​s​e​e​ ​o​p​t​i​o​n​s​ ​l​i​k​e​ ​T​a​b​l​e​ ​o​r​ ​M​T​U​ ​i​t​ ​m​e​a​n​s​ ​w​e​ ​d​o​ ​n​o​t​ ​s​u​p​p​o​r​t​ ​i​t​ ​f​o​r​ ​n​o​w​,​ ​b​u​t​ ​w​i​l​l​ ​b​e​ ​a​d​d​e​d​ ​l​a​t​e​r​.​<​/​p​> + */ + subTitle: string + card: { + /** + * S​e​t​t​i​n​g​ ​U​p​ ​A​ ​n​e​w​ ​T​u​n​n​e​l​: + */ + title: string + /** + * + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​p​>​1​.​ ​I​m​p​o​r​t​ ​C​o​n​f​i​g​u​r​a​t​i​o​n​ ​F​i​l​e​<​/​p​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​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​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​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​p​>​2​.​ ​O​r​ ​F​i​l​l​ ​i​n​ ​F​o​r​m​ ​o​n​ ​t​h​e​ ​L​e​f​t​<​/​p​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​d​i​v​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​u​l​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​l​i​>​ ​E​n​t​e​r​ ​a​ ​n​a​m​e​ ​f​o​r​ ​t​h​e​ ​t​u​n​n​e​l​.​<​/​l​i​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​l​i​>​ ​P​r​o​v​i​d​e​ ​e​s​s​e​n​t​i​a​l​ ​d​e​t​a​i​l​s​ ​s​u​c​h​ ​a​s​ ​t​h​e​ ​p​r​i​v​a​t​e​ ​k​e​y​,​ ​p​u​b​l​i​c​ ​k​e​y​,​ ​a​n​d​ ​e​n​d​p​o​i​n​t​ ​(​s​e​r​v​e​r​ ​a​d​d​r​e​s​s​)​.​<​/​l​i​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​/​u​l​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​/​d​i​v​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​p​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​F​o​r​ ​m​o​r​e​ ​h​e​l​p​,​ ​p​l​e​a​s​e​ ​v​i​s​i​t​ ​d​e​f​g​u​a​r​d​ ​h​e​l​p​ ​(​h​t​t​p​s​:​/​/​d​e​f​g​u​a​r​d​.​g​i​t​b​o​o​k​.​i​o​/​)​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​/​p​>​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ + */ + content: string + } } } addInstancePage: { @@ -353,13 +902,25 @@ type RootTranslation = { } sideBar: { /** - * I​n​s​t​a​n​c​e​s + * d​e​f​g​u​a​r​d​ ​I​n​s​t​a​n​c​e​s */ instances: string /** * A​d​d​ ​I​n​s​t​a​n​c​e */ addInstance: string + /** + * A​d​d​ ​T​u​n​n​e​l + */ + addTunnel: string + /** + * W​i​r​e​G​u​a​r​d​ ​T​u​n​n​e​l​s + */ + tunnels: 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 @@ -371,6 +932,24 @@ type RootTranslation = { */ appVersion: RequiredParams<'version'> } + /** + * A​p​p​l​i​c​a​t​i​o​n​ ​v​e​r​s​i​o​n​:​ + */ + applicationVersion: string + } + newApplicationVersion: { + /** + * N​e​w​ ​v​e​r​s​i​o​n​ ​a​v​a​i​l​a​b​l​e + */ + header: string + /** + * D​i​s​m​i​s​s + */ + dismiss: string + /** + * S​e​e​ ​w​h​a​t​'​s​ ​n​e​w + */ + releaseNotes: string } } enrollment: { @@ -433,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​. @@ -546,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: { @@ -592,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​>​ */ @@ -631,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 /** @@ -702,33 +1281,215 @@ type RootTranslation = { title: string messageBox: { /** - * Y​o​u​ ​c​a​n​ ​f​i​n​d​ ​t​o​k​e​n​ ​i​n​ ​e​-​m​a​i​l​ ​m​e​s​s​a​g​e​ ​o​r​ ​u​s​e​ ​d​i​r​e​c​t​ ​l​i​n​k​. + * Y​o​u​ ​c​a​n​ ​f​i​n​d​ ​t​o​k​e​n​ ​i​n​ ​e​-​m​a​i​l​ ​m​e​s​s​a​g​e​ ​o​r​ ​u​s​e​ ​d​i​r​e​c​t​ ​l​i​n​k​. + */ + email: string + } + form: { + errors: { + token: { + /** + * T​o​k​e​n​ ​i​s​ ​r​e​q​u​i​r​e​d + */ + required: string + } + } + fields: { + token: { + /** + * T​o​k​e​n + */ + placeholder: string + } + } + controls: { + /** + * N​e​x​t + */ + submit: string + } + } + } + } + } + modals: { + updateInstance: { + /** + * U​p​d​a​t​e​ ​i​n​s​t​a​n​c​e + */ + title: string + /** + * E​n​t​e​r​ ​t​h​e​ ​t​o​k​e​n​ ​s​e​n​t​ ​b​y​ ​t​h​e​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​ ​t​o​ ​u​p​d​a​t​e​ ​t​h​e​ ​I​n​s​t​a​n​c​e​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​.​ + ​A​l​t​e​r​n​a​t​i​v​e​l​y​,​ ​y​o​u​ ​c​a​n​ ​c​h​o​o​s​e​ ​t​o​ ​r​e​m​o​v​e​ ​t​h​i​s​ ​I​n​s​t​a​n​c​e​ ​e​n​t​i​r​e​l​y​ ​b​y​ ​c​l​i​c​k​i​n​g​ ​t​h​e​ ​'​R​e​m​o​v​e​ ​I​n​s​t​a​n​c​e​'​ ​b​u​t​t​o​n​ ​b​e​l​o​w​. + */ + infoMessage: string + form: { + fieldLabels: { + /** + * T​o​k​e​n + */ + token: string + /** + * U​R​L + */ + url: string + } + fieldErrors: { + token: { + /** + * T​o​k​e​n​ ​o​r​ ​U​R​L​ ​r​e​j​e​c​t​e​d​. + */ + rejected: string + /** + * I​n​s​t​a​n​c​e​ ​f​o​r​ ​t​h​i​s​ ​t​o​k​e​n​ ​w​a​s​ ​n​o​t​ ​f​o​u​n​d​. + */ + instanceIsNotPresent: string + } + } + } + controls: { + /** + * U​p​d​a​t​e​ ​I​n​s​t​a​n​c​e + */ + updateInstance: string + /** + * R​e​m​o​v​e​ ​I​n​s​t​a​n​c​e + */ + removeInstance: string + } + messages: { + /** + * {​n​a​m​e​}​ ​u​p​d​a​t​e​d​. + * @param {string} name + */ + success: RequiredParams<'name'> + /** + * T​o​k​e​n​ ​o​r​ ​U​R​L​ ​i​s​ ​i​n​v​a​l​i​d​. + */ + error: string + /** + * 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 + } + } + deleteInstance: { + /** + * D​e​l​e​t​e​ ​i​n​s​t​a​n​c​e + */ + 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: { + /** + * I​n​s​t​a​n​c​e​ ​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​r​e​d + */ + error: string + } + controls: { + /** + * D​e​l​e​t​e​ ​i​n​s​t​a​n​c​e + */ + 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​r​e​d + */ + error: string + } + controls: { + /** + * D​e​l​e​t​e​ ​t​u​n​n​e​l + */ + submit: string + } + } + mfa: { + authentication: { + /** + * T​w​o​-​f​a​c​t​o​r​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n + */ + title: string + /** + * P​a​s​t​e​ ​t​h​e​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​c​o​d​e​ ​f​r​o​m​ ​y​o​u​r​ ​A​u​t​h​e​n​t​i​c​a​t​o​r​ ​A​p​p​l​i​c​a​t​i​o​n​. + */ + authenticatorAppDescription: string + /** + * P​a​s​t​e​ ​t​h​e​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​c​o​d​e​ ​t​h​a​t​ ​w​a​s​ ​s​e​n​t​ ​t​o​ ​y​o​u​r​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s​. + */ + emailCodeDescription: string + /** + * F​o​r​ ​t​h​i​s​ ​c​o​n​n​e​c​t​i​o​n​,​ ​t​w​o​-​f​a​c​t​o​r​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​(​2​F​A​)​ ​i​s​ ​m​a​n​d​a​t​o​r​y​. + */ + mfaStartDescriptionPrimary: string + /** + * S​e​l​e​c​t​ ​y​o​u​r​ ​p​r​e​f​e​r​r​e​d​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​m​e​t​h​o​d​. + */ + mfaStartDescriptionSecondary: string + /** + * U​s​e​ ​a​u​t​h​e​n​t​i​c​a​t​o​r​ ​a​p​p + */ + useAuthenticatorApp: string + /** + * U​s​e​ ​y​o​u​r​ ​e​m​a​i​l​ ​c​o​d​e + */ + useEmailCode: string + /** + * U​s​e​ ​t​h​i​s​ ​m​e​t​h​o​d​ ​f​o​r​ ​f​u​t​u​r​e​ ​l​o​g​i​n​s + */ + saveAuthenticationMethodForFutureLogins: string + /** + * V​e​r​i​f​y + */ + buttonSubmit: string + errors: { + /** + * S​e​l​e​c​t​e​d​ ​m​e​t​h​o​d​ ​h​a​s​ ​n​o​t​ ​b​e​e​n​ ​c​o​n​f​i​g​u​r​e​d​. */ - email: string - } - form: { - errors: { - token: { - /** - * T​o​k​e​n​ ​i​s​ ​r​e​q​u​i​r​e​d - */ - required: string - } - } - fields: { - token: { - /** - * T​o​k​e​n - */ - placeholder: string - } - } - controls: { - /** - * N​e​x​t - */ - submit: string - } + mfaNotConfigured: string + /** + * C​o​u​l​d​ ​n​o​t​ ​s​t​a​r​t​ ​M​F​A​ ​p​r​o​c​e​s​s​.​ ​P​l​e​a​s​e​ ​t​r​y​ ​a​g​a​i​n​ ​o​r​ ​c​o​n​t​a​c​t​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​. + */ + mfaStartGeneric: string + /** + * C​o​u​l​d​ ​n​o​t​ ​f​i​n​d​ ​i​n​s​t​a​n​c​e​. + */ + instanceNotFound: string + /** + * L​o​c​a​t​i​o​n​ ​i​s​ ​n​o​t​ ​s​p​e​c​i​f​i​e​d​. + */ + locationNotSpecified: string + /** + * E​r​r​o​r​,​ ​t​h​i​s​ ​c​o​d​e​ ​i​s​ ​i​n​v​a​l​i​d​,​ ​t​r​y​ ​a​g​a​i​n​ ​o​r​ ​c​o​n​t​a​c​t​ ​y​o​u​r​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​. + */ + invalidCode: string + /** + * T​o​k​e​n​ ​h​a​s​ ​e​x​p​i​r​e​d​.​ ​P​l​e​a​s​e​ ​t​r​y​ ​t​o​ ​c​o​n​n​e​c​t​ ​a​g​a​i​n​. + */ + tokenExpired: string } } } @@ -745,7 +1506,7 @@ export type TranslationFunctions = { /** * seconds */ - prular: () => LocalizedString + plural: () => LocalizedString } minutes: { /** @@ -755,7 +1516,7 @@ export type TranslationFunctions = { /** * minutes */ - prular: () => LocalizedString + plural: () => LocalizedString } } form: { @@ -857,6 +1618,239 @@ 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 + } + tunnel: { + /** + * Add Tunnel + */ + 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: + */ + 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 + /** + * Support Us! + */ + supportUs: () => LocalizedString + } + } + } + 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 + } + } + versionUpdate: { + /** + * Updates + */ + title: () => LocalizedString + /** + * Check for updates + */ + checkboxTitle: () => LocalizedString + } + } + } + } + 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 @@ -885,7 +1879,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 } @@ -895,6 +1893,10 @@ export type TranslationFunctions = { * Locations */ title: () => LocalizedString + /** + * Edit Instance + */ + edit: () => LocalizedString filters: { views: { /** @@ -930,60 +1932,367 @@ export type TranslationFunctions = { */ active: () => LocalizedString /** - * Never connected + * Never connected + */ + neverConnected: () => LocalizedString + } + locationNeverConnected: { + /** + * Never Connected + */ + title: () => LocalizedString + /** + * This device was never connected to this location, connect to view statistics and information about connection + */ + content: () => LocalizedString + } + LocationNoStats: { + /** + * No stats + */ + title: () => LocalizedString + /** + * This device has no stats for this location in specified time period. Connect to location and wait for client to gather statistics. + */ + content: () => LocalizedString + } + detailView: { + history: { + /** + * Connection history + */ + title: () => LocalizedString + headers: { + /** + * Date + */ + date: () => LocalizedString + /** + * Duration + */ + duration: () => LocalizedString + /** + * Connected from + */ + connectedFrom: () => LocalizedString + /** + * Upload + */ + upload: () => LocalizedString + /** + * Download + */ + download: () => LocalizedString + } + } + details: { + /** + * Details + */ + title: () => LocalizedString + logs: { + /** + * Log + */ + title: () => LocalizedString + } + info: { + configuration: { + /** + * Device configuration + */ + title: () => LocalizedString + /** + * Public key + */ + pubkey: () => LocalizedString + /** + * Addresses + */ + address: () => LocalizedString + /** + * Listen port + */ + listenPort: () => LocalizedString + } + vpn: { + /** + * VPN Server Configuration + */ + title: () => LocalizedString + /** + * Public key + */ + pubkey: () => LocalizedString + /** + * Server Address + */ + serverAddress: () => LocalizedString + /** + * Allowed IPs + */ + allowedIps: () => LocalizedString + /** + * DNS servers + */ + dns: () => LocalizedString + /** + * Persistent keepalive + */ + keepalive: () => LocalizedString + /** + * Latest Handshake + */ + handshake: () => LocalizedString + /** + * {seconds} seconds ago + */ + handshakeValue: (arg: { seconds: number }) => LocalizedString + } + } + } + } + } + tunnelPage: { + /** + * WireGuard Tunnels + */ + title: () => LocalizedString + header: { + /** + * Edit Tunnel */ - neverConnected: () => LocalizedString + edit: () => LocalizedString } - locationNeverConnected: { + } + editTunnelPage: { + /** + * Edit WireGuard® Tunnel + */ + title: () => LocalizedString + messages: { /** - * Never Connected + * Tunnel edited */ - title: () => LocalizedString + editSuccess: () => LocalizedString /** - * This device was never connected to this location, connect to view statistics and information about connection + * Editing tunnel failed */ - content: () => LocalizedString + editError: () => LocalizedString } - LocationNoStats: { - /** - * No stats - */ - title: () => LocalizedString + controls: { /** - * This device has no stats for this location in specified time period. Connect to location and wait for client to gather statistics. + * Save changes */ - content: () => LocalizedString + save: () => LocalizedString } - detailView: { - history: { + } + addTunnelPage: { + /** + * Add WireGuard® Tunnel + */ + title: () => LocalizedString + forms: { + initTunnel: { /** - * Connection history + * Please provide Instance URL and token */ title: () => LocalizedString - headers: { + sections: { /** - * Date + * VPN Server */ - date: () => LocalizedString + vpnServer: () => LocalizedString /** - * Duration + * Advanced Options */ - duration: () => LocalizedString + advancedOptions: () => LocalizedString + } + labels: { /** - * Connected from + * Tunnel Name */ - connectedFrom: () => LocalizedString + name: () => LocalizedString /** - * Upload + * Private Key */ - upload: () => LocalizedString + privateKey: () => LocalizedString /** - * Download + * Public Key */ - download: () => LocalizedString + publicKey: () => LocalizedString + /** + * Address + */ + address: () => LocalizedString + /** + * Public Key + */ + serverPubkey: () => LocalizedString + /** + * VPN Server Address:Port + */ + endpoint: () => LocalizedString + /** + * DNS + */ + dns: () => LocalizedString + /** + * Allowed IPs (separate with comma) + */ + allowedips: () => LocalizedString + /** + * Persistent Keep Alive (sec) + */ + persistentKeepAlive: () => LocalizedString + /** + * PreUp + */ + preUp: () => LocalizedString + /** + * PostUp + */ + postUp: () => LocalizedString + /** + * PreDown + */ + PreDown: () => LocalizedString + /** + * PostDown + */ + PostDown: () => LocalizedString + } + helpers: { + /** + * Click the "Advanced Options" section to reveal additional settings for fine-tuning your WireGuard tunnel configuration. You can customize pre and post scripts, among other options. + */ + advancedOptions: () => LocalizedString + /** + * A unique name for your WireGuard tunnel to identify it easily. + */ + name: () => LocalizedString + /** + * The public key associated with the WireGuard tunnel for secure communication. + */ + pubkey: () => LocalizedString + /** + * The private key associated with the WireGuard tunnel for secure communication. + */ + prvkey: () => LocalizedString + /** + * The IP address assigned to this WireGuard client within the VPN network. + */ + address: () => LocalizedString + /** + * The public key of the WireGuard server for secure communication. + */ + serverPubkey: () => LocalizedString + /** + * A comma-separated list of IP addresses or CIDR ranges that are allowed for communication through the tunnel. + */ + allowedIps: () => LocalizedString + /** + * The address and port of the WireGuard server, typically in the format "hostname:port". + */ + endpoint: () => LocalizedString + /** + * 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 + /** + * The interval (in seconds) for sending periodic keep-alive messages to ensure the tunnel stays active. Adjust as needed. + */ + persistentKeepAlive: () => LocalizedString + /** + * If enabled, all network traffic will be routed through the WireGuard tunnel. + */ + routeAllTraffic: () => LocalizedString + /** + * Shell commands or scripts to be executed before bringing up the WireGuard tunnel. + */ + preUp: () => LocalizedString + /** + * Shell commands or scripts to be executed after bringing up the WireGuard tunnel. + */ + postUp: () => LocalizedString + /** + * Shell commands or scripts to be executed before tearing down the WireGuard tunnel. + */ + preDown: () => LocalizedString + /** + * Shell commands or scripts to be executed after tearing down the WireGuard tunnel. + */ + postDown: () => LocalizedString + } + /** + * Add Tunnel + */ + submit: () => LocalizedString + messages: { + /** + * Error parsing config file + */ + configError: () => LocalizedString + /** + * Tunnel added + */ + addSuccess: () => LocalizedString + /** + * Creating tunnel failed + */ + addError: () => LocalizedString + } + controls: { + /** + * Import Config File + */ + importConfig: () => LocalizedString + /** + * Generate Private Key + */ + generatePrvkey: () => LocalizedString } } } + guide: { + /** + * Adding WireGuard tunnel + */ + title: () => LocalizedString + /** + *

To establish secure communication between two or more devices over the internet create a virtual private network by configuring your tunnel.

If you don’t see options like Table or MTU it means we do not support it for now, but will be added later.

+ */ + subTitle: () => LocalizedString + card: { + /** + * Setting Up A new Tunnel: + */ + title: () => LocalizedString + /** + * +

1. Import Configuration File

+
+
    +
  • Click on the "Import Config File" button.
  • +
  • Navigate to configuration file using the file selection dialog.
  • +
  • Select the .conf file you received or created.
  • +
+
+

2. Or Fill in Form on the Left

+
+
    +
  • Enter a name for the tunnel.
  • +
  • Provide essential details such as the private key, public key, and endpoint (server address).
  • +
+
+

+ For more help, please visit defguard help (https://defguard.gitbook.io/) +

+ + */ + content: () => LocalizedString + } + } } addInstancePage: { /** @@ -1074,13 +2383,25 @@ export type TranslationFunctions = { } sideBar: { /** - * Instances + * defguard Instances */ instances: () => LocalizedString /** * Add Instance */ addInstance: () => LocalizedString + /** + * Add Tunnel + */ + addTunnel: () => LocalizedString + /** + * WireGuard Tunnels + */ + tunnels: () => LocalizedString + /** + * Settings + */ + settings: () => LocalizedString copyright: { /** * Copyright © 2023 @@ -1091,6 +2412,24 @@ export type TranslationFunctions = { */ appVersion: (arg: { version: string }) => LocalizedString } + /** + * Application version: + */ + applicationVersion: () => LocalizedString + } + newApplicationVersion: { + /** + * New version available + */ + header: () => LocalizedString + /** + * Dismiss + */ + dismiss: () => LocalizedString + /** + * See what's new + */ + releaseNotes: () => LocalizedString } } enrollment: { @@ -1152,7 +2491,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. @@ -1264,7 +2603,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: { @@ -1310,7 +2649,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.

*/ @@ -1348,7 +2687,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 /** @@ -1450,6 +2789,185 @@ export type TranslationFunctions = { } } } + modals: { + updateInstance: { + /** + * Update instance + */ + title: () => LocalizedString + /** + * Enter the token sent by the administrator to update the Instance configuration. + Alternatively, you can choose to remove this Instance entirely by clicking the 'Remove Instance' button below. + */ + infoMessage: () => LocalizedString + form: { + fieldLabels: { + /** + * Token + */ + token: () => LocalizedString + /** + * URL + */ + url: () => LocalizedString + } + fieldErrors: { + token: { + /** + * Token or URL rejected. + */ + rejected: () => LocalizedString + /** + * Instance for this token was not found. + */ + instanceIsNotPresent: () => LocalizedString + } + } + } + controls: { + /** + * Update Instance + */ + updateInstance: () => LocalizedString + /** + * Remove Instance + */ + removeInstance: () => LocalizedString + } + messages: { + /** + * {name} updated. + */ + success: (arg: { name: string }) => LocalizedString + /** + * Token or URL is invalid. + */ + error: () => LocalizedString + /** + * Instance for given token is not registered ! + */ + errorInstanceNotFound: () => LocalizedString + } + } + deleteInstance: { + /** + * Delete instance + */ + title: () => LocalizedString + /** + * Are you sure you want to delete {name}? + */ + subtitle: (arg: { name: string }) => LocalizedString + messages: { + /** + * Instance deleted + */ + success: () => LocalizedString + /** + * Unexpected error occurred + */ + error: () => LocalizedString + } + controls: { + /** + * Delete instance + */ + 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 occurred + */ + error: () => LocalizedString + } + controls: { + /** + * Delete tunnel + */ + submit: () => LocalizedString + } + } + mfa: { + authentication: { + /** + * Two-factor authentication + */ + title: () => LocalizedString + /** + * Paste the authentication code from your Authenticator Application. + */ + authenticatorAppDescription: () => LocalizedString + /** + * Paste the authentication code that was sent to your email address. + */ + emailCodeDescription: () => LocalizedString + /** + * For this connection, two-factor authentication (2FA) is mandatory. + */ + mfaStartDescriptionPrimary: () => LocalizedString + /** + * Select your preferred authentication method. + */ + mfaStartDescriptionSecondary: () => LocalizedString + /** + * Use authenticator app + */ + useAuthenticatorApp: () => LocalizedString + /** + * Use your email code + */ + useEmailCode: () => LocalizedString + /** + * Use this method for future logins + */ + saveAuthenticationMethodForFutureLogins: () => LocalizedString + /** + * Verify + */ + buttonSubmit: () => LocalizedString + errors: { + /** + * Selected method has not been configured. + */ + mfaNotConfigured: () => LocalizedString + /** + * Could not start MFA process. Please try again or contact administrator. + */ + mfaStartGeneric: () => LocalizedString + /** + * Could not find instance. + */ + instanceNotFound: () => LocalizedString + /** + * Location is not specified. + */ + locationNotSpecified: () => LocalizedString + /** + * Error, this code is invalid, try again or contact your administrator. + */ + invalidCode: () => LocalizedString + /** + * Token has expired. Please try to connect again. + */ + tokenExpired: () => LocalizedString + } + } + } + } } export type Formatters = {} diff --git a/src/pages/client/ClientPage.tsx b/src/pages/client/ClientPage.tsx index 3e1d9159..a7ee7781 100644 --- a/src/pages/client/ClientPage.tsx +++ b/src/pages/client/ClientPage.tsx @@ -3,19 +3,27 @@ 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'; -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 navigate = useNavigate(); + const firstLaunch = useClientFlags((state) => state.firstStart); + const location = useLocation(); const { data: instances } = useQuery({ queryFn: getInstances, @@ -23,12 +31,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 +57,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 +75,7 @@ export const ClientPage = () => { clientQueryKeys.getConnectionHistory, clientQueryKeys.getLocationStats, clientQueryKeys.getInstances, + clientQueryKeys.getTunnels, ]; invalidate.forEach((key) => queryClient.invalidateQueries({ @@ -77,7 +96,17 @@ export const ClientPage = () => { if (instances) { setInstances(instances); } - }, [instances, setInstances]); + if (tunnels) { + setTunnels(tunnels); + } + }, [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 407893e2..15585991 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -3,15 +3,27 @@ import { InvokeArgs } from '@tauri-apps/api/tauri'; import pTimeout from 'p-timeout'; import { debug, error, trace } from 'tauri-plugin-log-api'; -import { Connection, DefguardInstance, DefguardLocation, LocationStats } from '../types'; +import { NewApplicationVersionInfo } from '../../../shared/hooks/api/types'; +import { + CommonWireguardFields, + Connection, + DefguardInstance, + LocationStats, + Tunnel, +} from '../types'; import { ConnectionRequest, GetLocationsRequest, + LocationDetails, + LocationDetailsRequest, RoutingRequest, SaveConfigRequest, SaveDeviceConfigResponse, + Settings, StatsRequest, TauriCommandKey, + TunnelRequest, + UpdateInstnaceRequest, } from './types'; // Streamlines logging for invokes @@ -20,7 +32,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), { @@ -41,8 +52,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); @@ -65,8 +77,46 @@ 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 }); + +const deleteInstance = async (id: number): Promise => + invokeWrapper('delete_instance', { instanceId: id }); + +const updateInstance = async (data: UpdateInstnaceRequest): Promise => + invokeWrapper('update_instance', data); + +const parseTunnelConfig = async (config: string) => + invokeWrapper('parse_tunnel_config', { config: config }); + +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'); + +// 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 }); + +const deleteTunnel = async (id: number): Promise => + invokeWrapper('delete_tunnel', { tunnelId: id }); + +const getLatestAppVersion = async (): Promise => + invokeWrapper('get_latest_app_version'); + export const clientApi = { getInstances, + getTunnels, getLocations, connect, disconnect, @@ -76,4 +126,15 @@ export const clientApi = { getActiveConnection, saveConfig, updateLocationRouting, + getSettings, + updateSettings, + deleteInstance, + deleteTunnel, + getLocationDetails, + updateInstance, + parseTunnelConfig, + saveTunnel, + openLink, + getTunnelDetails, + getLatestAppVersion, }; diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 2ff59bf5..7458b400 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -1,5 +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; @@ -7,15 +8,19 @@ export type GetLocationsRequest = { export type ConnectionRequest = { locationId: number; + connectionType: WireguardInstanceType; + presharedKey?: string; }; export type RoutingRequest = { locationId: number; + connectionType: WireguardInstanceType; routeAllTraffic?: boolean; }; export type StatsRequest = { locationId: number; + connectionType: WireguardInstanceType; from?: string; }; @@ -24,10 +29,82 @@ export type SaveConfigRequest = { response: CreateDeviceResponse; }; +export type UpdateInstnaceRequest = { + instanceId: number; + response: CreateDeviceResponse; +}; + export type SaveDeviceConfigResponse = { instance: DefguardInstance; locations: DefguardLocation[]; }; +export type SaveTunnelRequest = { + privateKey: string; + response: CreateDeviceResponse; +}; + +export type TrayIconTheme = 'color' | 'white' | 'black' | 'gray'; + +export type LogLevel = 'error' | 'info' | 'debug' | 'trace'; + +export type LogItemField = { + message: string; + interface_name?: string; +}; + +export type LogItem = { + // datetime UTC + timestamp: string; + level: LogLevel; + target: string; + fields: LogItemField; +}; + +export type InterfaceLogsRequest = { + locationId: DefguardLocation['id']; +}; + +export type Settings = { + theme: ThemeKey; + log_level: LogLevel; + tray_icon_theme: TrayIconTheme; + check_for_updates: boolean; +}; + +export type LocationDetails = { + location_id: number; + name: string; + pubkey: string; + address: string; + dns?: string; + listen_port: number; + peer_pubkey: string; + peer_endpoint: string; + allowed_ips: string; + persistent_keepalive_interval?: number; + 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; + connectionType: WireguardInstanceType; +}; export type TauriCommandKey = | 'all_instances' @@ -39,4 +116,16 @@ export type TauriCommandKey = | 'all_connections' | 'active_connection' | 'save_device_config' - | 'update_location_routing'; + | 'update_location_routing' + | 'get_settings' + | 'update_settings' + | 'delete_instance' + | 'update_instance' + | 'parse_tunnel_config' + | 'save_tunnel' + | 'all_tunnels' + | 'tunnel_details' + | 'delete_tunnel' + | 'location_interface_details' + | 'open_link' + | 'get_latest_app_version'; diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index f3ae4689..77063541 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -1,41 +1,160 @@ import './style.scss'; -import { useNavigate } from 'react-router-dom'; +import { getVersion } from '@tauri-apps/api/app'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; +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'; +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'; import { routes } from '../../../../shared/routes'; +import { clientApi } from '../../clientAPI/clientApi'; import { useClientStore } from '../../hooks/useClientStore'; +import { WireguardInstanceType } from '../../types'; import { ClientBarItem } from './components/ClientBarItem/ClientBarItem'; +import { NewApplicationVersionAvailableInfo } from './components/NewApplicationVersionAvailableInfo/NewApplicationVersionAvailableInfo'; + +const { openLink } = clientApi; export const ClientSideBar = () => { + const navigate = useNavigate(); const { LL } = useI18nContext(); - const instances = useClientStore((state) => state.instances); + 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 (
-
- +
navigate(routes.client.carousel, { replace: true })} + > +
-
+
navigate(routes.client.carousel, { replace: true })} + >
-
+

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

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

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

+
+ {tunnels.map((tunnel) => ( + + ))} + +
+ + + + +
+
+
+ ); +}; + +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(); + const pathActive = useMatch(routes.client.settings); + return ( +
{ + navigate(routes.client.settings, { replace: true }); + }} + > + +

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

); }; @@ -58,3 +177,21 @@ const AddInstance = () => {
); }; +const AddTunnel = () => { + const { LL } = useI18nContext(); + const navigate = useNavigate(); + return ( +
{ + navigate(routes.client.addTunnel, { replace: true }); + }} + > + + + +

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

+
+ ); +}; diff --git a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx index 7acdf16e..969ecd57 100644 --- a/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx +++ b/src/pages/client/components/ClientSideBar/components/ClientBarItem/ClientBarItem.tsx @@ -1,24 +1,43 @@ 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'; import { routes } from '../../../../../../shared/routes'; import { useClientStore } from '../../../../hooks/useClientStore'; -import { DefguardInstance } from '../../../../types'; +import { WireguardInstanceType } from '../../../../types'; type Props = { - instance: DefguardInstance; + 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 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: instance.id === selectedInstance, - connected: instance.connected, + active: itemSelected, + connected: acitve, }); const { refs, floatingStyles } = useFloating({ @@ -33,20 +52,37 @@ export const ClientBarItem = ({ instance }: Props) => { className={cn} ref={refs.setReference} onClick={() => { - setClientStore({ selectedInstance: instance.id }); + 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.connected && ( + {acitve && (
{ + 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 ac55b290..e463ed24 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; @@ -24,10 +27,17 @@ height: 108px; column-gap: 7px; border-bottom: 1px solid var(--border-primary); + cursor: pointer; @include media-breakpoint-up(lg) { display: flex; } + + :nth-child(2) { + path { + fill: var(--text-body-primary); + } + } } & > .logo-mobile { @@ -39,6 +49,7 @@ align-items: center; justify-content: center; box-sizing: border-box; + cursor: pointer; @include media-breakpoint-up(lg) { display: none; @@ -52,11 +63,20 @@ & > .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; @@ -66,7 +86,8 @@ padding-top: 70px; } - & > .client-bar-item { + & > .client-bar-item, + & > div > .client-bar-item { display: grid; box-sizing: border-box; width: 100%; @@ -89,7 +110,7 @@ & > svg, & > .icon-wrapper { - display: none; + margin-bottom: 20px; grid-column: 1; grid-row: 1; width: 40px; @@ -99,6 +120,7 @@ display: flex; width: 24px; height: 24px; + margin-bottom: 0; } } @@ -201,6 +223,10 @@ } } + #settings-nav-item { + // margin-top: auto; + } + #add-instance { @include media-breakpoint-down(lg) { display: grid; @@ -210,13 +236,16 @@ justify-content: center; padding: 0; } + & > .icon-wrapper { display: flex; + svg { width: 15px; height: 15px; } } + & > p { display: none; @@ -236,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/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/hooks/useClientStore.tsx b/src/pages/client/hooks/useClientStore.tsx index 75ebdd2e..4ce69add 100644 --- a/src/pages/client/hooks/useClientStore.tsx +++ b/src/pages/client/hooks/useClientStore.tsx @@ -2,16 +2,30 @@ import { isUndefined } from 'lodash-es'; import { createWithEqualityFn } from 'zustand/traditional'; import { clientApi } from '../clientAPI/clientApi'; -import { ClientView, DefguardInstance } from '../types'; +import { Settings } from '../clientAPI/types'; +import { + ClientView, + CommonWireguardFields, + DefguardInstance, + SelectedInstance, + WireguardInstanceType, +} from '../types'; -const { getInstances } = clientApi; +const { getInstances, updateSettings } = clientApi; // eslint-disable-next-line const defaultValues: StoreValues = { instances: [], + tunnels: [], selectedInstance: undefined, statsFilter: 1, selectedView: ClientView.GRID, + settings: { + log_level: 'error', + theme: 'light', + tray_icon_theme: 'color', + check_for_updates: true, + }, }; export const useClientStore = createWithEqualityFn( @@ -20,24 +34,43 @@ 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 }); }, + 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; // 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 }); }, + updateSettings: async (data) => { + const res = await updateSettings(data); + set({ settings: res }); + }, }), Object.is, ); @@ -46,13 +79,17 @@ type Store = StoreValues & StoreMethods; type StoreValues = { instances: DefguardInstance[]; + tunnels: CommonWireguardFields[]; selectedView: ClientView; statsFilter: number; - selectedInstance?: DefguardInstance['id']; + settings: Settings; + selectedInstance?: SelectedInstance; }; 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/CarouselPage/CarouselPage.tsx b/src/pages/client/pages/CarouselPage/CarouselPage.tsx new file mode 100644 index 00000000..1a823276 --- /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..5a899736 --- /dev/null +++ b/src/pages/client/pages/CarouselPage/cards/CarouselCards.tsx @@ -0,0 +1,234 @@ +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 { 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 { 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.tunnel.title()}

+

{localLL.tunnel.subtitle()}

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

+ {localLL.title()} +

+
+ +
+ {localLL.sideText()} +
+
+ +
+ ); +}; + +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..bbbb3621 --- /dev/null +++ b/src/pages/client/pages/CarouselPage/style.scss @@ -0,0 +1,223 @@ +@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; + } + } + a { + text-decoration: underline; + cursor: pointer; + } + + .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; + min-height: 72px; + } + + strong, + b { + font-weight: 700; + } + + .row { + width: 100%; + 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(xxl) { + 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 { + padding-bottom: 40px; + display: block; + } + + & > .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, + #welcome-slide { + min-height: 750px; + } + + #factor-slide, + #security-slide, + #instances-slide, + #support-slide { + justify-content: space-between; + row-gap: 25px; + } + + #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/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceDeviceForm/AddInstanceDeviceForm.tsx index 49850671..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 @@ -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,8 +103,13 @@ export const AddInstanceDeviceForm = ({ response }: Props) => { .then((res) => { setIsLoading(false); toaster.success(localLL.messages.addSuccess()); - setClientStore({ selectedInstance: res.instance.id }); - navigate(routes.client.base, { replace: true }); + setClientStore({ + selectedInstance: { + id: res.instance.id, + type: WireguardInstanceType.DEFGUARD_INSTANCE, + }, + }); + navigate(routes.client.instanceCreated, { replace: true }); }) .catch(() => { toaster.error(LL.common.messages.error()); 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/ClientAddInstancePage/components/AddInstanceGuide/AddInstanceGuide.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceGuide/AddInstanceGuide.tsx index a12f6216..d029378b 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceGuide/AddInstanceGuide.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceGuide/AddInstanceGuide.tsx @@ -17,7 +17,7 @@ export const AddInstanceGuide = () => {
-

{localLL.card.title()}:

+

{localLL.card.title()}

{parse(localLL.card.content())}
diff --git a/src/pages/client/pages/ClientAddInstancePage/style.scss b/src/pages/client/pages/ClientAddInstancePage/style.scss index 2b1d9ac5..f60ddf13 100644 --- a/src/pages/client/pages/ClientAddInstancePage/style.scss +++ b/src/pages/client/pages/ClientAddInstancePage/style.scss @@ -21,6 +21,7 @@ flex-flow: row; align-items: center; justify-content: center; + .btn { width: 100%; max-width: 200px; @@ -42,30 +43,33 @@ } & > .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/AddTunnelGuide/AddTunnelGuide.tsx b/src/pages/client/pages/ClientAddTunnelPage/AddTunnelGuide/AddTunnelGuide.tsx new file mode 100644 index 00000000..ff8ccd80 --- /dev/null +++ b/src/pages/client/pages/ClientAddTunnelPage/AddTunnelGuide/AddTunnelGuide.tsx @@ -0,0 +1,25 @@ +import './style.scss'; + +import parse from 'html-react-parser'; + +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import SvgVpnLocation from '../../../../../shared/components/svg/VpnLocation'; +import { Card } from '../../../../../shared/defguard-ui/components/Layout/Card/Card'; + +export const AddTunnelGuide = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.addTunnelPage.guide; + return ( +
+
+

{localLL.title()}

+ {parse(localLL.subTitle())} +
+ + +

{localLL.card.title()}:

+ {parse(localLL.card.content())} +
+
+ ); +}; diff --git a/src/pages/client/pages/ClientAddTunnelPage/AddTunnelGuide/style.scss b/src/pages/client/pages/ClientAddTunnelPage/AddTunnelGuide/style.scss new file mode 100644 index 00000000..1a5f64e7 --- /dev/null +++ b/src/pages/client/pages/ClientAddTunnelPage/AddTunnelGuide/style.scss @@ -0,0 +1,55 @@ +@use '@scssutils' as *; + +#add-tunnel-guide { + display: flex; + flex-flow: column; + align-items: center; + justify-content: flex-start; + row-gap: 50px; + + p { + @include typography(app-body-2); + + color: var(--text-body-primary); + } + a { + @include typography(app-body-2); + + color: var(--text-body-primary); + } + li { + @include typography(app-body-2); + + color: var(--text-body-primary); + } + + #tunnel-guide { + * { + text-align: center; + width: 100%; + } + + h2 { + padding-bottom: 20px; + } + + p { + max-width: 500px; + } + } + + #setup-guide { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-content: flex-start; + row-gap: 20px; + box-sizing: border-box; + padding: 20px 25px; + + & > div { + box-sizing: border-box; + padding-left: 20px; + } + } +} diff --git a/src/pages/client/pages/ClientAddTunnelPage/ClientAddTunnelPage.tsx b/src/pages/client/pages/ClientAddTunnelPage/ClientAddTunnelPage.tsx new file mode 100644 index 00000000..2d173a1a --- /dev/null +++ b/src/pages/client/pages/ClientAddTunnelPage/ClientAddTunnelPage.tsx @@ -0,0 +1,20 @@ +import './style.scss'; + +import { useI18nContext } from '../../../../i18n/i18n-react'; +import { AddTunnelGuide } from './AddTunnelGuide/AddTunnelGuide'; +import { AddTunnelFormCard } from './components/AddTunnelFormCard/AddTunnelFormCard'; + +export const ClientAddTunnelPage = () => { + const { LL } = useI18nContext(); + return ( +
+
+

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

+
+
+ + +
+
+ ); +}; diff --git a/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx new file mode 100644 index 00000000..93b94970 --- /dev/null +++ b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/AddTunnelFormCard.tsx @@ -0,0 +1,331 @@ +import './style.scss'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { pickBy } from 'lodash-es'; +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'; +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 { 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 { 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 { generateWGKeys } from '../../../../../../shared/utils/generateWGKeys'; +import { validateIpOrDomainList } from '../../../../../../shared/validators/tunnel'; +import { clientApi } from '../../../../clientAPI/clientApi'; + +type FormFields = { + 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: '', +}; + +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 */ + const schema = useMemo( + () => + z.object({ + 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 && value.length != 0) { + 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.tunnelCreated, { replace: true }); + toaster.success(localLL.messages.addSuccess()); + }) + .catch(() => toaster.error(localLL.messages.addError())); + }; + const { handleSubmit, control, reset, setValue } = useForm({ + resolver: zodResolver(schema), + defaultValues, + mode: 'all', + }); + + const [generatedKeys, setGeneratedKeys] = useState(false); + + const handleConfigUpload = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = false; + input.style.display = 'none'; + input.onchange = () => { + if (input.files && input.files.length === 1) { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result && input.files) { + const res = reader.result; + parseTunnelConfig(res as string) + .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())); + } + }; + reader.onerror = () => { + toaster.error(localLL.messages.configError()); + }; + reader.readAsText(input.files[0]); + } + }; + input.click(); + }; + + const generateKeyPair = () => { + const { privateKey, publicKey } = generateWGKeys(); + setValue('prvkey', privateKey); + setValue('pubkey', publicKey); + setGeneratedKeys(true); + }; + + useEffect(() => { + // eslint-disable-next-line + const onPrvKeyChange = (e: any) => { + if (generatedKeys && e.target.value !== defaultValues.prvkey) { + setGeneratedKeys(false); + } + }; + + const prvKeyInput = document.getElementsByName('prvkey')[0]; + if (prvKeyInput) { + prvKeyInput.addEventListener('input', onPrvKeyChange); + + return () => { + prvKeyInput.removeEventListener('input', onPrvKeyChange); + }; + } + }, [generatedKeys]); + + 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/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss new file mode 100644 index 00000000..be3a2aa7 --- /dev/null +++ b/src/pages/client/pages/ClientAddTunnelPage/components/AddTunnelFormCard/style.scss @@ -0,0 +1,34 @@ +@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; + + & > .btn { + min-width: 135px; + + span { + display: block; + padding: 0 1px; + } + } + } + } +} diff --git a/src/pages/client/pages/ClientAddTunnelPage/style.scss b/src/pages/client/pages/ClientAddTunnelPage/style.scss new file mode 100644 index 00000000..96f01a35 --- /dev/null +++ b/src/pages/client/pages/ClientAddTunnelPage/style.scss @@ -0,0 +1,127 @@ +@use '@scssutils' as *; + +#client-add-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); + } + + form { + & > * { + width: 100%; + } + + .controls { + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + + .btn { + width: 100%; + max-width: 200px; + } + } + } + + & > header { + width: 100%; + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + padding-bottom: 15px; + + & > h1 { + text-align: left; + } + } + + & > .content { + 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%; + flex-grow: 1; + } + + & > .card { + box-sizing: border-box; + padding: 32px 64px; + + 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; + } + + .advanced-options.open { + display: block; + } + + > .controls { + padding-top: 42px; + + .btn { + height: 47px; + } + } + } + } + } +} 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/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx b/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx new file mode 100644 index 00000000..e59be110 --- /dev/null +++ b/src/pages/client/pages/ClientEditTunnelPage/ClientEditTunnelPage.tsx @@ -0,0 +1,86 @@ +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'; +import { DeleteTunnelModal } from './modals/DeleteTunnelModal/DeleteTunnelModal'; +import { useDeleteTunnelModal } from './modals/DeleteTunnelModal/useDeleteTunnelModal'; + +const { getTunnelDetails } = clientApi; + +export const ClientEditTunnelPage = () => { + const { LL } = useI18nContext(); + const navigate = useNavigate(); + const submitRef = useRef(null); + const selectedInstance = useClientStore((state) => state.selectedInstance); + const openDeleteTunnel = useDeleteTunnelModal((state) => state.open); + 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/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; +}; 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 9051cbdc..57bdf9d1 100644 --- a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx +++ b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx @@ -1,24 +1,99 @@ import './style.scss'; +import { isUndefined } from 'lodash-es'; +import { useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; + import { useI18nContext } from '../../../../i18n/i18n-react'; +import { Button } from '../../../../shared/defguard-ui/components/Layout/Button/Button'; +import { ButtonStyleVariant } from '../../../../shared/defguard-ui/components/Layout/Button/types'; +import { routes } from '../../../../shared/routes'; +import { useClientStore } from '../../hooks/useClientStore'; +import { DefguardInstance, WireguardInstanceType } from '../../types'; import { LocationsList } from './components/LocationsList/LocationsList'; import { StatsFilterSelect } from './components/StatsFilterSelect/StatsFilterSelect'; import { StatsLayoutSelect } from './components/StatsLayoutSelect/StatsLayoutSelect'; +import { DeleteInstanceModal } from './modals/DeleteInstanceModal/DeleteInstanceModal'; +import { UpdateInstanceModal } from './modals/UpdateInstanceModal/UpdateInstanceModal'; +import { useUpdateInstanceModal } from './modals/UpdateInstanceModal/useUpdateInstanceModal'; export const ClientInstancePage = () => { const { LL } = useI18nContext(); - const pageLL = LL.pages.client.pages.instancePage; + const instanceLL = LL.pages.client.pages.instancePage; + const tunnelLL = 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((): 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); + + useEffect(() => { + 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, selectedInstanceType, tunnels.length, navigate]); return (
-

{pageLL.title()}

+

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

- + {isLocationPage && ( + <> + + {selectedInstance && ( +
+ +
); }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx index fa83e2f6..fe81c867 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx @@ -1,16 +1,19 @@ import { useQuery } from '@tanstack/react-query'; -import { useEffect } from 'react'; +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'; -import { ClientView } from '../../../../types'; +import { ClientView, WireguardInstanceType } from '../../../../types'; import { LocationsDetailView } from './components/LocationsDetailView/LocationsDetailView'; import { LocationsGridView } from './components/LocationsGridView/LocationsGridView'; +import { MFAModal } from './modals/MFAModal/MFAModal'; -const { getLocations } = clientApi; +const { getLocations, getTunnels } = clientApi; export const LocationsList = () => { const { LL } = useI18nContext(); @@ -20,9 +23,27 @@ 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]; + } 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, }); @@ -32,17 +53,30 @@ 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 && ( - - )} + {selectedView === ClientView.GRID && } + {selectedView === ClientView.DETAIL && ( - + )} + + ); }; 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..d28f4528 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,18 +14,20 @@ 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'; +import { useMFAModal } from '../../modals/MFAModal/useMFAModal'; const { connect, disconnect } = clientApi; type Props = { - location?: DefguardLocation; + location?: CommonWireguardFields; }; export const LocationCardConnectButton = ({ location }: Props) => { const toaster = useToaster(); const [isLoading, setIsLoading] = useState(false); const { LL } = useI18nContext(); + const openMFAModal = useMFAModal((state) => state.open); const cn = classNames('location-card-connect-button', { connected: location?.active, @@ -36,11 +38,19 @@ export const LocationCardConnectButton = ({ location }: Props) => { try { if (location) { if (location?.active) { - await disconnect({ locationId: location.id }); - } else { - await connect({ - locationId: location?.id, + await disconnect({ + locationId: location.id, + connectionType: location.connection_type, }); + } else { + if (location.mfa_enabled) { + openMFAModal(location); + } 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 5eace993..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,10 +6,14 @@ 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, + WireguardInstanceType, +} from '../../../../../../types'; type Props = { - location?: DefguardLocation; + location?: CommonWireguardFields; connection?: Connection; }; @@ -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/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/LocationCardRoute/LocationCardRoute.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx index 10207af6..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 @@ -7,19 +7,20 @@ 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; 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/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 b4d5c251..d4b8d470 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,81 +1,44 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; -import parse from 'html-react-parser'; -import { useMemo, useState } from 'react'; -import { useBreakpoint } from 'use-breakpoint'; +import { isUndefined } from 'lodash-es'; +import { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; -import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; -import { deviceBreakpoints } from '../../../../../../../../shared/constants'; -import { Card } from '../../../../../../../../shared/defguard-ui/components/Layout/Card/Card'; import { CardTabs } from '../../../../../../../../shared/defguard-ui/components/Layout/CardTabs/CardTabs'; import { CardTabsData } from '../../../../../../../../shared/defguard-ui/components/Layout/CardTabs/types'; -import { Helper } from '../../../../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; -import { Label } from '../../../../../../../../shared/defguard-ui/components/Layout/Label/Label'; -import { getStatsFilterValue } from '../../../../../../../../shared/utils/getStatsFilterValue'; +import { routes } from '../../../../../../../../shared/routes'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../../../query'; -import { DefguardInstance, DefguardLocation } from '../../../../../../types'; -import { LocationUsageChart } from '../../../LocationUsageChart/LocationUsageChart'; -import { LocationUsageChartType } from '../../../LocationUsageChart/types'; -import { LocationCardConnectButton } from '../LocationCardConnectButton/LocationCardConnectButton'; -import { LocationCardInfo } from '../LocationCardInfo/LocationCardInfo'; -import { LocationCardNeverConnected } from '../LocationCardNeverConnected/LocationCardNeverConnected'; -import { LocationCardNoStats } from '../LocationCardNoStats/LocationCardNoStats'; -import { LocationCardRoute } from '../LocationCardRoute/LocationCardRoute'; -import { LocationCardTitle } from '../LocationCardTitle/LocationCardTitle'; -import { LocationConnectionHistory } from '../LocationConnectionHistory/LocationConnectionHistory'; +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 = { - instanceId: DefguardInstance['id']; - locations: DefguardLocation[]; + locations: CommonWireguardFields[]; + connectionType?: WireguardInstanceType; }; const findLocationById = ( - locations: DefguardLocation[], + locations: CommonWireguardFields[], id: number, -): DefguardLocation | undefined => locations.find((location) => location.id === id); - -const { getLocationStats, getLastConnection, getConnectionHistory } = clientApi; - -export const LocationsDetailView = ({ locations }: Props) => { - const { LL } = useI18nContext(); - const localLL = LL.pages.client.pages.instancePage; - const { breakpoint } = useBreakpoint({ ...deviceBreakpoints, desktop: 1300 }); - const [activeLocationId, setActiveLocationId] = useState(locations[0].id); - const statsFilter = useClientStore((state) => state.statsFilter); - - const { data: locationStats } = useQuery({ - queryKey: [clientQueryKeys.getLocationStats, activeLocationId as number, statsFilter], - queryFn: () => - getLocationStats({ - locationId: activeLocationId as number, - from: getStatsFilterValue(statsFilter), - }), - enabled: !!activeLocationId, - refetchInterval: 10 * 1000, - refetchOnWindowFocus: true, - refetchOnMount: true, - }); +): CommonWireguardFields | undefined => locations.find((location) => location.id === id); - const { data: connectionHistory } = useQuery({ - queryKey: [clientQueryKeys.getConnectionHistory, activeLocationId as number], - queryFn: () => getConnectionHistory({ locationId: activeLocationId as number }), - enabled: !!activeLocationId, - refetchInterval: 10 * 1000, - refetchOnWindowFocus: true, - refetchOnMount: true, - }); +const { getTunnels } = clientApi; - const { data: lastConnection } = useQuery({ - queryKey: [clientQueryKeys.getConnections, activeLocationId as number], - queryFn: () => getLastConnection({ locationId: activeLocationId as number }), - enabled: !!activeLocationId, - refetchInterval: 10 * 1000, - refetchOnWindowFocus: true, - refetchOnMount: true, - }); +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( (): CardTabsData[] => @@ -88,108 +51,77 @@ export const LocationsDetailView = ({ locations }: Props) => { [locations, activeLocationId], ); - const activeLocation = useMemo( - (): DefguardLocation | undefined => findLocationById(locations, activeLocationId), - [locations, activeLocationId], - ); + const activeLocation = useMemo((): CommonWireguardFields | undefined => { + if (!isUndefined(activeLocationId)) { + return findLocationById(locations, activeLocationId); + } + return undefined; + }, [locations, activeLocationId]); + + useEffect(() => { + if (activeLocationId === undefined) { + navigate(routes.client.addInstance, { replace: true }); + } + }, [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); + + // select first location if selected is undefined but component is mounted + useEffect(() => { + if ((!activeLocationId || !activeLocation) && !isUndefined(locations[0])) { + setActiveLocationId(locations[0].id); + } + }, [locations, setActiveLocationId, activeLocationId, activeLocation]); + + if (isUndefined(activeLocationId) || isUndefined(activeLocation)) return null; return (
- - -
- - {breakpoint === 'desktop' && ( - + {connectionType === WireguardInstanceType.DEFGUARD_INSTANCE && ( + <> + + {activeLocation && } + {activeLocation && ( + + )} + {activeLocation && ( + )} - {breakpoint === 'desktop' && ( -
- {!activeLocation?.active && ( -
- - {parse(localLL.controls.traffic.helper())} - - -
- )} - {activeLocation?.active && ( - <> - -

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

- - )} -
+ + )} + {connectionType === WireguardInstanceType.TUNNEL && ( + <> + {tunnel && } + {tunnel && ( + )} - -
- {breakpoint !== 'desktop' && ( -
- -
- )} - {breakpoint !== 'desktop' && ( -
-
- - - - - - } - > - {parse(LL.pages.client.pages.instancePage.controls.traffic.helper())} - -
- -
- )} - {locationStats && locationStats.length > 0 && ( - - )} - {(!locationStats || locationStats.length == 0) && - ((connectionHistory && connectionHistory.length) || activeLocation?.active) && ( - )} - {connectionHistory && connectionHistory.length ? ( - <> -

Connection History

- - - ) : null} - {(!connectionHistory || - (connectionHistory.length === 0 && !activeLocation?.active)) && ( - - )} -
+ + )}
); }; 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 new file mode 100644 index 00000000..5e5b9801 --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx @@ -0,0 +1,55 @@ +import './style.scss'; + +import { useQuery } from '@tanstack/react-query'; + +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, 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, + 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, connectionType }), + enabled: !!locationId, + refetchInterval: 10 * 1000, + refetchOnWindowFocus: true, + refetchOnMount: true, + }); + + if (!connectionHistory) return null; + + return ( + +
+

{localLL.title()}

+
+ {connectionHistory.length === 0 && !connected && ( +
+ +
+ )} + {connectionHistory.length > 0 && ( + + )} +
+ ); +}; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationConnectionHistory/LocationConnectionHistory.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationHistoryTable/LocationHistoryTable.tsx similarity index 89% rename from src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationConnectionHistory/LocationConnectionHistory.tsx rename to src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationHistoryTable/LocationHistoryTable.tsx index 12defec2..f4d0eb43 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationConnectionHistory/LocationConnectionHistory.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationHistoryTable/LocationHistoryTable.tsx @@ -4,14 +4,14 @@ import dayjs from 'dayjs'; import { floor, isUndefined } from 'lodash-es'; import { ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'; -import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; +import { useI18nContext } from '../../../../../../../../../../../i18n/i18n-react'; import { ListHeader, ListRowCell, ListSortDirection, -} from '../../../../../../../../shared/defguard-ui/components/Layout/VirtualizedList/types'; -import { VirtualizedList } from '../../../../../../../../shared/defguard-ui/components/Layout/VirtualizedList/VirtualizedList'; -import { Connection } from '../../../../../../types'; +} from '../../../../../../../../../../../shared/defguard-ui/components/Layout/VirtualizedList/types'; +import { VirtualizedList } from '../../../../../../../../../../../shared/defguard-ui/components/Layout/VirtualizedList/VirtualizedList'; +import { Connection } from '../../../../../../../../../types'; type Props = { connections: Connection[]; @@ -31,7 +31,7 @@ const getDuration = (start: string, end: string): string => { } }; -export const LocationConnectionHistory = ({ connections }: Props) => { +export const LocationHistoryTable = ({ connections }: Props) => { const { LL } = useI18nContext(); const pageLL = LL.pages.client.pages.instancePage.detailView.history.headers; const connectionsLength = useRef(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 new file mode 100644 index 00000000..226a6f52 --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/style.scss @@ -0,0 +1,66 @@ +@use '@scssutils' as *; + +@mixin list-layout { + display: inline-grid; + grid-template-columns: 1.5fr repeat(4, 1fr); + + & > * { + grid-row: 1; + } +} + +#connection-history-card { + margin-bottom: 50px; + + & > header { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + height: 30px; + } + + .connections-list { + max-height: 400px; + grid-template-rows: 28px 1fr; + box-sizing: border-box; + margin-top: 20px; + + .headers { + @include list-layout; + } + + .scroll-container { + overflow-y: auto; + padding: 0; + margin-right: 5px; + grid-row: 2; + grid-column: 1; + } + + .custom-row { + @include list-layout; + + align-items: center; + grid-template-rows: 1fr; + height: 20px; + cursor: pointer; + width: 100%; + box-sizing: border-box; + + span { + @include typography(app-button-xl); + color: var(--text-body-primary); + } + + .date { + @include typography(app-strap); + } + } + } + + & > .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 new file mode 100644 index 00000000..951d90a9 --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/LocationDetailCard.tsx @@ -0,0 +1,159 @@ +import './style.scss'; + +import { useQuery } from '@tanstack/react-query'; +import classNames from 'classnames'; +import parse from 'html-react-parser'; +import { memo } from 'react'; +import { Label } from 'recharts'; +import { useBreakpoint } from 'use-breakpoint'; + +import { useI18nContext } from '../../../../../../../../../../i18n/i18n-react'; +import { deviceBreakpoints } from '../../../../../../../../../../shared/constants'; +import { Card } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { Helper } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; +import { getStatsFilterValue } from '../../../../../../../../../../shared/utils/getStatsFilterValue'; +import { clientApi } from '../../../../../../../../clientAPI/clientApi'; +import { useClientStore } from '../../../../../../../../hooks/useClientStore'; +import { clientQueryKeys } from '../../../../../../../../query'; +import { CommonWireguardFields } from '../../../../../../../../types'; +import { LocationUsageChart } from '../../../../../LocationUsageChart/LocationUsageChart'; +import { LocationUsageChartType } from '../../../../../LocationUsageChart/types'; +import { LocationCardConnectButton } from '../../../LocationCardConnectButton/LocationCardConnectButton'; +import { LocationCardInfo } from '../../../LocationCardInfo/LocationCardInfo'; +import { LocationCardNoStats } from '../../../LocationCardNoStats/LocationCardNoStats'; +import { LocationCardRoute } from '../../../LocationCardRoute/LocationCardRoute'; +import { LocationCardTitle } from '../../../LocationCardTitle/LocationCardTitle'; + +type Props = { + location: CommonWireguardFields; + tabbed?: boolean; +}; + +const { getLocationStats, getLastConnection } = clientApi; + +export const LocationDetailCard = memo(({ location, tabbed = false }: Props) => { + const { LL } = useI18nContext(); + const { breakpoint } = useBreakpoint({ ...deviceBreakpoints, desktop: 1300 }); + const localLL = LL.pages.client.pages.instancePage; + const statsFilter = useClientStore((state) => state.statsFilter); + + const { data: locationStats } = useQuery({ + queryKey: [ + clientQueryKeys.getLocationStats, + location.id, + statsFilter, + location.connection_type, + ], + queryFn: () => + getLocationStats({ + locationId: location.id, + connectionType: location.connection_type, + from: getStatsFilterValue(statsFilter), + }), + enabled: !!location, + refetchInterval: 10 * 1000, + refetchOnWindowFocus: true, + refetchOnMount: true, + }); + + const { data: lastConnection } = useQuery({ + queryKey: [clientQueryKeys.getConnections, location.id, location.connection_type], + queryFn: () => + getLastConnection({ + locationId: location.id, + connectionType: location.connection_type, + }), + enabled: !!location, + refetchInterval: 10 * 1000, + refetchOnWindowFocus: true, + refetchOnMount: true, + }); + + return ( + +
+ + {breakpoint === 'desktop' && ( + + )} + {breakpoint === 'desktop' && ( +
+ {!location?.active && ( +
+ + {parse(localLL.controls.traffic.helper())} + + +
+ )} + {location?.active && ( +
+ +

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

+
+ )} +
+ )} + +
+ {breakpoint !== 'desktop' && ( +
+ +
+ )} + {breakpoint !== 'desktop' && ( +
+
+ + + + + + } + > + {parse(LL.pages.client.pages.instancePage.controls.traffic.helper())} + +
+ +
+ )} + {locationStats && locationStats.length > 0 ? ( + + ) : ( +
+ +
+ )} +
+ ); +}); 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 new file mode 100644 index 00000000..6149cc70 --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetailCard/style.scss @@ -0,0 +1,131 @@ +@use '@scssutils' as *; + +#locations-detail-view { + .detail-card { + display: flex; + flex-flow: column; + row-gap: 20px; + padding: 20px 0; + overflow: hidden; + max-height: none; + margin-bottom: 20px; + + &.tabbed { + border-top-left-radius: 0; + } + + & > .location-no-stats { + padding-top: 50px; + } + + & > .header { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + width: 100%; + row-gap: 20px; + box-sizing: border-box; + padding: 0 20px; + + .route { + .controls { + position: relative; + + .helper { + position: absolute; + top: -10px; + right: -20px; + } + } + } + } + + .toggle { + .toggle-option { + height: 32px; + + span { + text-align: left; + @include typography(app-button-s); + } + } + } + + & > .grid-item { + box-sizing: border-box; + padding: 20px 25px; + min-height: 245px; + + & > .top { + width: 100%; + display: flex; + flex-flow: row nowrap; + align-items: flex-start; + + & > .btn { + margin-left: auto; + height: 40px; + } + } + + & > .info { + margin: 32px 0; + display: flex; + flex-flow: row; + align-items: flex-start; + justify-content: space-between; + } + + & > .no-data { + width: 100%; + display: block; + text-align: center; + margin-top: 42px; + + @include typography(app-button-xl); + + color: var(--text-body-primary); + } + } + + & > .location-usage { + box-sizing: border-box; + padding: 0 20px; + height: clamp(100px, 400px, 25vh); + } + + & > .info { + display: flex; + flex-flow: row wrap; + column-gap: 10px; + row-gap: 20px; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + padding: 0 20px; + } + + & > .route { + display: flex; + flex-flow: column; + row-gap: 8px; + box-sizing: border-box; + padding: 0 20px; + + .top { + display: flex; + flex-flow: row nowrap; + column-gap: 0; + align-items: center; + 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/LocationDetails/LocationDetails.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx new file mode 100644 index 00000000..3c7f9abb --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx @@ -0,0 +1,119 @@ +import './style.scss'; + +import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; +import { memo } from 'react'; + +import { useI18nContext } from '../../../../../../../../../../i18n/i18n-react'; +import { Card } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { Divider } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Divider/Divider'; +import { Label } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Label/Label'; +import { clientApi } from '../../../../../../../../clientAPI/clientApi'; +import { clientQueryKeys } from '../../../../../../../../query'; +import { DefguardLocation, WireguardInstanceType } from '../../../../../../../../types'; +import { LocationLogs } from '../LocationLogs/LocationLogs'; + +type Props = { + locationId: DefguardLocation['id']; + connectionType: WireguardInstanceType; +}; + +const { getLocationDetails } = clientApi; + +export const LocationDetails = ({ locationId, connectionType }: Props) => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.instancePage.detailView.details; + + return ( + +
+

{localLL.title()}

+
+ + +
+ ); +}; + +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, connectionType], + queryFn: () => getLocationDetails({ locationId, connectionType }), + enabled: !!locationId, + refetchInterval: 1000, + }); + + const handshakeString = () => { + if (data) { + const handshake = data.last_handshake && dayjs.unix(data.last_handshake); + const now = dayjs(); + return localLL.info.vpn.handshakeValue({ seconds: now.diff(handshake, 'seconds') }); + } + return ''; + }; + + return ( + <> +
+

{localLL.info.configuration.title()}

+
+ +
+

{data?.peer_pubkey}

+
+
+
+ +
+

{data?.peer_endpoint}

+
+
+
+ +
+

{data?.listen_port}

+
+
+
+ +
+

{localLL.info.vpn.title()}

+
+ +
+

{data?.pubkey}

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

{ip}

)} +
+
+
+ +
+

+ {data && data.dns && data.dns.split(',').map((d) =>

{d}

)} +

+
+
+
+ +
+

{data?.persistent_keepalive_interval ?? ''}

+
+
+
+ +
+

{handshakeString()}

+
+
+
+ + ); +}); diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/style.scss b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/style.scss new file mode 100644 index 00000000..b8226538 --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/style.scss @@ -0,0 +1,63 @@ +@use '@scssutils' as *; + +#location-details-card { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-content: flex-start; + row-gap: 20px; + + & > * { + width: 100%; + } + + .info-section { + display: flex; + flex-flow: row wrap; + align-items: flex-start; + justify-content: flex-start; + row-gap: 8px; + column-gap: 30px; + + h3 { + width: 100%; + } + + & > .info { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-content: flex-start; + flex-grow: 0; + row-gap: 8px; + + & > label { + user-select: none; + } + + & > .values { + display: flex; + flex-flow: row wrap; + align-items: flex-start; + justify-content: flex-start; + gap: 8px; + overflow: hidden; + + &.ips { + max-width: 250px; + } + + &.pubkey { + max-width: 380px; + } + + & > * { + @include typography(app-button-xl); + color: var(--text-body-primary); + max-width: 100%; + user-select: auto; + } + } + } + } +} 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 new file mode 100644 index 00000000..4a54e485 --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx @@ -0,0 +1,153 @@ +import './style.scss'; + +import { clipboard } from '@tauri-apps/api'; +import { save } from '@tauri-apps/api/dialog'; +import { listen, UnlistenFn } from '@tauri-apps/api/event'; +import { writeTextFile } from '@tauri-apps/api/fs'; +import { isUndefined } from 'lodash-es'; +import { useCallback, useEffect, useRef } from 'react'; + +import { useI18nContext } from '../../../../../../../../../../i18n/i18n-react'; +import { ActionButton } from '../../../../../../../../../../shared/defguard-ui/components/Layout/ActionButton/ActionButton'; +import { ActionButtonVariant } from '../../../../../../../../../../shared/defguard-ui/components/Layout/ActionButton/types'; +import { Card } from '../../../../../../../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { LogItem, LogLevel } from '../../../../../../../../clientAPI/types'; +import { useClientStore } from '../../../../../../../../hooks/useClientStore'; +import { DefguardLocation, WireguardInstanceType } from '../../../../../../../../types'; +import { LocationLogsSelect } from './LocationLogsSelect'; + +type Props = { + locationId: DefguardLocation['id']; + connectionType: WireguardInstanceType; +}; + +export const LocationLogs = ({ locationId, connectionType }: Props) => { + const logsContainerElement = useRef(null); + const appLogLevel = useClientStore((state) => state.settings.log_level); + const locationLogLevelRef = useRef(appLogLevel); + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.instancePage.detailView.details.logs; + + const handleLogsDownload = async () => { + const path = await save({}); + if (path) { + const logs = getAllLogs(); + await writeTextFile(path, logs); + } + }; + + const clearLogs = useCallback(() => { + if (logsContainerElement.current) { + logsContainerElement.current.innerHTML = ''; + } + }, []); + + // Clear logs when the component is unmounted or locationId changes + useEffect(() => { + return () => clearLogs(); + }, [clearLogs, locationId]); + + // Listen to new logs + useEffect(() => { + let eventUnlisten: UnlistenFn; + const startLogListen = async () => { + eventUnlisten = await listen( + `log-update-${connectionType}-${locationId}`, + ({ payload: logItems }) => { + if (logsContainerElement.current) { + logItems.forEach((item) => { + if ( + logsContainerElement.current && + filterLogByLevel(locationLogLevelRef.current, item.level) + ) { + const messageString = `${item.timestamp} ${item.level} ${item.fields.message}`; + const element = createLogLineElement(messageString); + const scrollAfterAppend = + logsContainerElement.current.scrollHeight - + logsContainerElement.current.scrollTop === + logsContainerElement.current.clientHeight; + logsContainerElement.current.appendChild(element); + // auto scroll to bottom if user didn't scroll up + if (scrollAfterAppend) { + logsContainerElement.current.scrollTo({ + top: logsContainerElement.current.scrollHeight, + }); + } + } + }); + } + }, + ); + }; + if (!isUndefined(locationId)) { + startLogListen(); + } + //unsubscribe on dismount + return () => { + eventUnlisten?.(); + }; + //eslint-disable-next-line + }, [locationId]); + + const getAllLogs = () => { + let logs = ''; + + if (logsContainerElement) { + logsContainerElement.current?.childNodes.forEach((item) => { + logs += item.textContent + '\n'; + }); + } + + return logs; + }; + + return ( + +
+

{localLL.title()}

+ { + locationLogLevelRef.current = level; + }} + /> + { + const logs = getAllLogs(); + if (logs) { + clipboard.writeText(logs); + } + }} + /> + +
+
+
+ ); +}; + +const createLogLineElement = (content: string): HTMLParagraphElement => { + const element = document.createElement('p'); + element.classList.add('log-line'); + element.textContent = content; + return element; +}; + +// 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_level === 'error'; + case 'info': + return ['info', 'error'].includes(log_level); + case 'debug': + return ['error', 'info', 'debug'].includes(log_level); + case 'trace': + return true; + } +}; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogsSelect.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogsSelect.tsx new file mode 100644 index 00000000..55b0659c --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogsSelect.tsx @@ -0,0 +1,76 @@ +import { useCallback, useMemo, useState } 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 { LogLevel } from '../../../../../../../../clientAPI/types'; + +type Props = { + initSelected: LogLevel; + onChange: (selected: LogLevel) => void; +}; + +export const LocationLogsSelect = ({ initSelected, onChange }: Props) => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.settingsPage.tabs.global.logging.options; + const [selected, setSelected] = useState(initSelected); + + const options = useMemo((): SelectOption[] => { + return [ + { + key: 0, + label: localLL.error(), + value: 'error', + }, + { + key: 1, + label: localLL.info(), + value: 'info', + }, + { + key: 2, + label: localLL.debug(), + value: 'debug', + }, + { + key: 3, + label: localLL.trace(), + value: 'trace', + }, + ]; + }, [localLL]); + + const renderSelected = useCallback( + (value: LogLevel): SelectSelectedValue => { + const option = options.find((o) => o.value === value); + if (option) { + return { + key: option.key, + displayValue: option.label, + }; + } + return { + key: 0, + displayValue: '', + }; + }, + [options], + ); + + return ( + mutate({ theme })} + loading={isPending} + /> + ); +}; + +const LoggingLevelSelect = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.settingsPage.tabs.global.logging; + const settings = useClientStore((state) => state.settings); + const updateClientSettings = useClientStore((state) => state.updateSettings); + + const { mutate, isPending } = useMutation({ + mutationFn: updateClientSettings, + }); + + const loggingOptions = useMemo((): SelectOption[] => { + const res: SelectOption[] = [ + { + key: 0, + label: localLL.options.error(), + value: 'error', + }, + { + key: 1, + label: localLL.options.info(), + value: 'info', + }, + { + key: 2, + label: localLL.options.debug(), + value: 'debug', + }, + { + key: 3, + label: localLL.options.trace(), + value: 'trace', + }, + ]; + return res; + }, [localLL.options]); + + const renderSelected = useCallback( + (val: LogLevel) => { + const option = loggingOptions.find((o) => o.value === val); + if (option) { + return { + key: option.key, + displayValue: option.label, + }; + } + return { + key: 999, + displayValue: '', + }; + }, + [loggingOptions], + ); + + return ( + mutate({ tray_icon_theme: theme })} + loading={isPending} + /> + ); +}; + +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/pages/ClientSettingsPage/components/GlobalSettingsTab/style.scss b/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/style.scss new file mode 100644 index 00000000..7bfdfb1f --- /dev/null +++ b/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/style.scss @@ -0,0 +1,21 @@ +@use '@scssutils' as *; + +#global-settings-tab { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-items: flex-start; + row-gap: 32px; + + & > section { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-items: flex-start; + row-gap: 20px; + } + + .select-container { + min-width: 250px; + } +} 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..f3d46404 --- /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 cardImage from './assets/hero.png'; + +const { openLink } = clientApi; + +const GithubButton = () => { + const { LL } = useI18nContext(); + const localLL = LL.pages.client.pages.carouselPage.slides.shared; + return ( +