diff --git a/package.json b/package.json index 0ac7fe96..ec245aa7 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@tanstack/react-virtual": "3.0.0-beta.54", "@tauri-apps/api": "^1.6.0", "@types/byte-size": "^8.1.2", + "@use-gesture/react": "^10.3.1", "byte-size": "^8.2.1", "classnames": "^2.5.1", "clsx": "^2.1.1", @@ -64,6 +65,7 @@ "itertools": "^2.4.1", "lodash-es": "^4.17.21", "merge-refs": "^1.3.0", + "millify": "^6.1.0", "p-timeout": "^6.1.4", "prop-types": "^15.8.1", "radash": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6ad8890..2b1d0ada 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 + '@use-gesture/react': + specifier: ^10.3.1 + version: 10.3.1(react@18.3.1) byte-size: specifier: ^8.2.1 version: 8.2.1 @@ -80,6 +83,9 @@ importers: merge-refs: specifier: ^1.3.0 version: 1.3.0(@types/react@18.3.21) + millify: + specifier: ^6.1.0 + version: 6.1.0 p-timeout: specifier: ^6.1.4 version: 6.1.4 @@ -1085,6 +1091,14 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vitejs/plugin-react-swc@3.9.0': resolution: {integrity: sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==} peerDependencies: @@ -1271,6 +1285,10 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1507,6 +1525,9 @@ packages: electron-to-chromium@1.5.155: resolution: {integrity: sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1774,6 +1795,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1985,6 +2010,10 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-generator-function@1.1.0: resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} engines: {node: '>= 0.4'} @@ -2278,6 +2307,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + millify@6.1.0: + resolution: {integrity: sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==} + hasBin: true + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2624,6 +2657,10 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2771,6 +2808,10 @@ packages: spdx-license-ids@3.0.21: resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -3060,9 +3101,17 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3070,6 +3119,14 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3944,6 +4001,13 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + '@vitejs/plugin-react-swc@3.9.0(vite@4.5.14(@types/node@20.17.48)(sass@1.70.0))': dependencies: '@swc/core': 1.11.24 @@ -4171,6 +4235,12 @@ snapshots: classnames@2.5.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} color-convert@1.9.3: @@ -4404,6 +4474,8 @@ snapshots: electron-to-chromium@1.5.155: {} + emoji-regex@8.0.0: {} + entities@4.5.0: {} entities@6.0.0: {} @@ -4790,6 +4862,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5040,6 +5114,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.0: dependencies: call-bound: 1.0.4 @@ -5444,6 +5520,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + millify@6.1.0: + dependencies: + yargs: 17.7.2 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -5827,6 +5907,8 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + require-directory@2.1.1: {} + resolve-from@4.0.0: {} resolve@1.22.10: @@ -5992,6 +6074,12 @@ snapshots: spdx-license-ids@3.0.21: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 @@ -6353,12 +6441,32 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrappy@1.0.2: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yaml@1.10.2: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} zod@3.24.4: {} diff --git a/src-tauri/.sqlx/query-5953a81f34f906e34aabec089dfe0cebf2afc3ad798638db9ea0aabcd506192b.json b/src-tauri/.sqlx/query-5953a81f34f906e34aabec089dfe0cebf2afc3ad798638db9ea0aabcd506192b.json deleted file mode 100644 index ab312bb2..00000000 --- a/src-tauri/.sqlx/query-5953a81f34f906e34aabec089dfe0cebf2afc3ad798638db9ea0aabcd506192b.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,route_all_traffic, mfa_enabled, keepalive_interval FROM location;", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "instance_id", - "ordinal": 1, - "type_info": "Integer" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "address", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "pubkey", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "endpoint", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "allowed_ips", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "dns", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "network_id", - "ordinal": 8, - "type_info": "Integer" - }, - { - "name": "route_all_traffic", - "ordinal": 9, - "type_info": "Bool" - }, - { - "name": "mfa_enabled", - "ordinal": 10, - "type_info": "Bool" - }, - { - "name": "keepalive_interval", - "ordinal": 11, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false - ] - }, - "hash": "5953a81f34f906e34aabec089dfe0cebf2afc3ad798638db9ea0aabcd506192b" -} diff --git a/src-tauri/.sqlx/query-76998bbd9e0096deb957c8e60df4b7f10eb86c8341e3cc0bc081e5c4dcbcee44.json b/src-tauri/.sqlx/query-79c5c710bc12a1353875219d3cd2198b6c2cf5b476774034d3160a3f28df08d7.json similarity index 76% rename from src-tauri/.sqlx/query-76998bbd9e0096deb957c8e60df4b7f10eb86c8341e3cc0bc081e5c4dcbcee44.json rename to src-tauri/.sqlx/query-79c5c710bc12a1353875219d3cd2198b6c2cf5b476774034d3160a3f28df08d7.json index 5ed80c68..565650b6 100644 --- a/src-tauri/.sqlx/query-76998bbd9e0096deb957c8e60df4b7f10eb86c8341e3cc0bc081e5c4dcbcee44.json +++ b/src-tauri/.sqlx/query-79c5c710bc12a1353875219d3cd2198b6c2cf5b476774034d3160a3f28df08d7.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", disable_all_traffic, enterprise_enabled FROM instance;", + "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", disable_all_traffic, enterprise_enabled, use_openid_for_mfa, openid_display_name FROM instance;", "describe": { "columns": [ { @@ -47,6 +47,16 @@ "name": "enterprise_enabled", "ordinal": 8, "type_info": "Bool" + }, + { + "name": "use_openid_for_mfa", + "ordinal": 9, + "type_info": "Bool" + }, + { + "name": "openid_display_name", + "ordinal": 10, + "type_info": "Text" } ], "parameters": { @@ -61,8 +71,10 @@ false, true, false, - false + false, + false, + true ] }, - "hash": "76998bbd9e0096deb957c8e60df4b7f10eb86c8341e3cc0bc081e5c4dcbcee44" + "hash": "79c5c710bc12a1353875219d3cd2198b6c2cf5b476774034d3160a3f28df08d7" } diff --git a/src-tauri/.sqlx/query-c688e91a89197793b2a63dcefc41c652fc89286481300450763a7a9b66d146f9.json b/src-tauri/.sqlx/query-7f1db6e3022b3bef4515d183feb6dc2e3b787c1fe5c4ce26c1c14c356d0c85d5.json similarity index 75% rename from src-tauri/.sqlx/query-c688e91a89197793b2a63dcefc41c652fc89286481300450763a7a9b66d146f9.json rename to src-tauri/.sqlx/query-7f1db6e3022b3bef4515d183feb6dc2e3b787c1fe5c4ce26c1c14c356d0c85d5.json index b09e9828..dab05ae6 100644 --- a/src-tauri/.sqlx/query-c688e91a89197793b2a63dcefc41c652fc89286481300450763a7a9b66d146f9.json +++ b/src-tauri/.sqlx/query-7f1db6e3022b3bef4515d183feb6dc2e3b787c1fe5c4ce26c1c14c356d0c85d5.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", disable_all_traffic, enterprise_enabled FROM instance WHERE id = $1;", + "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", disable_all_traffic, enterprise_enabled, use_openid_for_mfa, openid_display_name FROM instance WHERE id = $1;", "describe": { "columns": [ { @@ -47,6 +47,16 @@ "name": "enterprise_enabled", "ordinal": 8, "type_info": "Bool" + }, + { + "name": "use_openid_for_mfa", + "ordinal": 9, + "type_info": "Bool" + }, + { + "name": "openid_display_name", + "ordinal": 10, + "type_info": "Text" } ], "parameters": { @@ -61,8 +71,10 @@ false, true, false, - false + false, + false, + true ] }, - "hash": "c688e91a89197793b2a63dcefc41c652fc89286481300450763a7a9b66d146f9" + "hash": "7f1db6e3022b3bef4515d183feb6dc2e3b787c1fe5c4ce26c1c14c356d0c85d5" } diff --git a/src-tauri/.sqlx/query-db3aa093e5e74398f5b921ddb6e833962b82c95d4b98993264cb55f9e96c81c0.json b/src-tauri/.sqlx/query-a490103ccb68cc382bd1fc84c2d1ebd6f344bbda5618980943a62677683f0a85.json similarity index 55% rename from src-tauri/.sqlx/query-db3aa093e5e74398f5b921ddb6e833962b82c95d4b98993264cb55f9e96c81c0.json rename to src-tauri/.sqlx/query-a490103ccb68cc382bd1fc84c2d1ebd6f344bbda5618980943a62677683f0a85.json index eb12a208..695d90f1 100644 --- a/src-tauri/.sqlx/query-db3aa093e5e74398f5b921ddb6e833962b82c95d4b98993264cb55f9e96c81c0.json +++ b/src-tauri/.sqlx/query-a490103ccb68cc382bd1fc84c2d1ebd6f344bbda5618980943a62677683f0a85.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "UPDATE instance SET name = $1, uuid = $2, url = $3, proxy_url = $4, username = $5, disable_all_traffic = $6, enterprise_enabled = $7, token = $8 WHERE id = $9;", + "query": "UPDATE instance SET name = $1, uuid = $2, url = $3, proxy_url = $4, username = $5, disable_all_traffic = $6, enterprise_enabled = $7, token = $8, use_openid_for_mfa = $9, openid_display_name = $10 WHERE id = $11;", "describe": { "columns": [], "parameters": { - "Right": 9 + "Right": 11 }, "nullable": [] }, - "hash": "db3aa093e5e74398f5b921ddb6e833962b82c95d4b98993264cb55f9e96c81c0" + "hash": "a490103ccb68cc382bd1fc84c2d1ebd6f344bbda5618980943a62677683f0a85" } diff --git a/src-tauri/.sqlx/query-d84dc04e42e2ef85f990b3f01c4db1ac59ec5e5940a7fa7ff1f6d2181a1f4763.json b/src-tauri/.sqlx/query-e50367bf0ea4627fb3125e541c2a2b2da96589b3aeb6b1c902f436ae98e9ffe9.json similarity index 71% rename from src-tauri/.sqlx/query-d84dc04e42e2ef85f990b3f01c4db1ac59ec5e5940a7fa7ff1f6d2181a1f4763.json rename to src-tauri/.sqlx/query-e50367bf0ea4627fb3125e541c2a2b2da96589b3aeb6b1c902f436ae98e9ffe9.json index 3898614a..0ed03e46 100644 --- a/src-tauri/.sqlx/query-d84dc04e42e2ef85f990b3f01c4db1ac59ec5e5940a7fa7ff1f6d2181a1f4763.json +++ b/src-tauri/.sqlx/query-e50367bf0ea4627fb3125e541c2a2b2da96589b3aeb6b1c902f436ae98e9ffe9.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token, disable_all_traffic, enterprise_enabled FROM instance\n WHERE token IS NOT NULL;", + "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token, disable_all_traffic, enterprise_enabled, use_openid_for_mfa, openid_display_name FROM instance\n WHERE token IS NOT NULL;", "describe": { "columns": [ { @@ -47,6 +47,16 @@ "name": "enterprise_enabled", "ordinal": 8, "type_info": "Bool" + }, + { + "name": "use_openid_for_mfa", + "ordinal": 9, + "type_info": "Bool" + }, + { + "name": "openid_display_name", + "ordinal": 10, + "type_info": "Text" } ], "parameters": { @@ -61,8 +71,10 @@ false, true, false, - false + false, + false, + true ] }, - "hash": "d84dc04e42e2ef85f990b3f01c4db1ac59ec5e5940a7fa7ff1f6d2181a1f4763" + "hash": "e50367bf0ea4627fb3125e541c2a2b2da96589b3aeb6b1c902f436ae98e9ffe9" } diff --git a/src-tauri/migrations/20250623083017_openid_mfa.sql b/src-tauri/migrations/20250623083017_openid_mfa.sql new file mode 100644 index 00000000..1bf588b7 --- /dev/null +++ b/src-tauri/migrations/20250623083017_openid_mfa.sql @@ -0,0 +1,2 @@ +ALTER TABLE instance ADD COLUMN use_openid_for_mfa BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE instance ADD COLUMN openid_display_name TEXT; diff --git a/src-tauri/proto b/src-tauri/proto index 6e1308a8..eb4ac062 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit 6e1308a8043d81b007a10a7f3e62f11b64e5435d +Subproject commit eb4ac0620f54bfa58669f2ac61ea5fce5c55b521 diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 738b4d96..182e14d6 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -308,6 +308,8 @@ pub async fn all_instances(app_state: State<'_, AppState>) -> Result { pub token: Option, pub disable_all_traffic: bool, pub enterprise_enabled: bool, + pub use_openid_for_mfa: bool, + pub openid_display_name: Option, } impl fmt::Display for Instance { @@ -37,6 +39,8 @@ impl From for Instance { token: None, disable_all_traffic: instance_info.disable_all_traffic, enterprise_enabled: instance_info.enterprise_enabled, + use_openid_for_mfa: instance_info.use_openid_for_mfa, + openid_display_name: instance_info.openid_display_name, } } } @@ -48,7 +52,7 @@ impl Instance { { query!( "UPDATE instance SET name = $1, uuid = $2, url = $3, proxy_url = $4, username = $5, \ - disable_all_traffic = $6, enterprise_enabled = $7, token = $8 WHERE id = $9;", + disable_all_traffic = $6, enterprise_enabled = $7, token = $8, use_openid_for_mfa = $9, openid_display_name = $10 WHERE id = $11;", self.name, self.uuid, self.url, @@ -57,6 +61,8 @@ impl Instance { self.disable_all_traffic, self.enterprise_enabled, self.token, + self.use_openid_for_mfa, + self.openid_display_name, self.id ) .execute(executor) @@ -71,7 +77,7 @@ impl Instance { let instances = query_as!( Self, "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", \ - disable_all_traffic, enterprise_enabled FROM instance;" + disable_all_traffic, enterprise_enabled, use_openid_for_mfa, openid_display_name FROM instance;" ) .fetch_all(executor) .await?; @@ -85,7 +91,7 @@ impl Instance { let instance = query_as!( Self, "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", \ - disable_all_traffic, enterprise_enabled FROM instance WHERE id = $1;", + disable_all_traffic, enterprise_enabled, use_openid_for_mfa, openid_display_name FROM instance WHERE id = $1;", id ) .fetch_optional(executor) @@ -119,7 +125,7 @@ impl Instance { let instances = query_as!( Self, "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token, \ - disable_all_traffic, enterprise_enabled FROM instance + disable_all_traffic, enterprise_enabled, use_openid_for_mfa, openid_display_name FROM instance WHERE token IS NOT NULL;" ) .fetch_all(executor) @@ -138,6 +144,8 @@ impl PartialEq for Instance { && self.username == other.username && self.disable_all_traffic == other.disable_all_traffic && self.enterprise_enabled == other.enterprise_enabled + && self.use_openid_for_mfa == other.use_openid_for_mfa + && self.openid_display_name == other.openid_display_name } } @@ -173,6 +181,8 @@ impl Instance { token: self.token, disable_all_traffic: self.disable_all_traffic, enterprise_enabled: self.enterprise_enabled, + use_openid_for_mfa: self.use_openid_for_mfa, + openid_display_name: self.openid_display_name, }) } } @@ -188,6 +198,8 @@ pub struct InstanceInfo { pub pubkey: String, pub disable_all_traffic: bool, pub enterprise_enabled: bool, + pub use_openid_for_mfa: bool, + pub openid_display_name: Option, } impl fmt::Display for InstanceInfo { diff --git a/src-tauri/src/log_watcher/global_log_watcher.rs b/src-tauri/src/log_watcher/global_log_watcher.rs index 88050426..58f01195 100644 --- a/src-tauri/src/log_watcher/global_log_watcher.rs +++ b/src-tauri/src/log_watcher/global_log_watcher.rs @@ -233,9 +233,7 @@ impl GlobalLogWatcher { break; } } else { - trace!("Read service log line: {service_line:?}"); if let Some(parsed_line) = self.parse_service_log_line(&service_line) { - trace!("Parsed service log line: {parsed_line:?}"); parsed_lines.push(parsed_line); } service_line.clear(); @@ -260,15 +258,13 @@ impl GlobalLogWatcher { if client_line_read > 0 { match self.parse_client_log_line(&client_line) { Ok(Some(parsed_line)) => { - trace!("Parsed client log line: {parsed_line:?}"); parsed_lines.push(parsed_line); } Ok(None) => { - trace!("The following log line was filtered out: {client_line:?}"); + // Line was filtered out, do nothing } - Err(err) => { - // trace here is intentional, adding error logs would loop the reader infinitely - trace!("Couldn't parse client log line: {client_line:?}: {err}"); + Err(_) => { + // Don't log it, as it will cause an endless loop } } client_line.clear(); @@ -300,45 +296,31 @@ impl GlobalLogWatcher { /// Deserializes the log line into a known struct. /// Also performs filtering by log level and optional timestamp. fn parse_service_log_line(&self, line: &str) -> Option { - trace!("Parsing service log line: {line}"); let Ok(mut log_line) = serde_json::from_str::(line) else { warn!("Failed to parse service log line: {line}"); return None; }; - trace!("Parsed service log line into: {log_line:?}"); // filter by log level if log_line.level > self.log_level { - trace!( - "Log level {} is above configured verbosity threshold {}. Skipping line...", - log_line.level, - self.log_level - ); return None; } // filter by optional timestamp if let Some(from) = self.from { if log_line.timestamp < from { - trace!( - "Timestamp {} is below configured threshold {from}. Skipping line...", - log_line.timestamp - ); return None; } } log_line.source = Some(LogSource::Service); - trace!("Successfully parsed service log line."); - Some(log_line) } /// Parse a client log line into a known struct using regex. /// If the line doesn't match the regex, it's filtered out. fn parse_client_log_line(&self, line: &str) -> Result, LogWatcherError> { - trace!("Parsing client log line: {line}"); // Example log: // [2024-10-09][09:08:41][DEBUG][defguard_client::commands] Retrieving all locations. let regex = Regex::new(r"\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\] (.*)")?; @@ -398,25 +380,15 @@ impl GlobalLogWatcher { }; if log_line.level > self.log_level { - trace!( - "Log level {} is above configured verbosity threshold {}. Skipping line...", - log_line.level, - self.log_level - ); return Ok(None); } if let Some(from) = self.from { if log_line.timestamp < from { - trace!("Timestamp is before configured threshold {from}. Skipping line..."); return Ok(None); } } - trace!( - "Successfully parsed client log line from file {:?}", - self.log_dirs.client_log_dir - ); Ok(Some(log_line)) } } diff --git a/src-tauri/src/service/mod.rs b/src-tauri/src/service/mod.rs index 171bf8e3..e418614e 100644 --- a/src-tauri/src/service/mod.rs +++ b/src-tauri/src/service/mod.rs @@ -94,6 +94,8 @@ pub fn setup_wgapi(ifname: &str) -> Result, Status> { #[tonic::async_trait] impl DesktopDaemonService for DaemonService { + type ReadInterfaceDataStream = InterfaceDataStream; + async fn create_interface( &self, request: tonic::Request, @@ -223,8 +225,6 @@ impl DesktopDaemonService for DaemonService { Ok(Response::new(())) } - type ReadInterfaceDataStream = InterfaceDataStream; - async fn read_interface_data( &self, request: tonic::Request, diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 5b6eb7f5..fd5b0b2f 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -722,6 +722,24 @@ If you want to disengage your VPN connection, simply press "deactivate". useEmailCode: 'Use your email code', saveAuthenticationMethodForFutureLogins: 'Use this method for future logins', buttonSubmit: 'Verify', + openidLogin: { + description: + 'In order to connect to the VPN please login with {provider}. To do so, please click "Authenticate with {provider}" button below.', + browserWarning: + '**This will open a new window in your Web Browser** and automatically redirect you to the {provider} login page. After authenticating with {provider} please get back here.', + buttonText: 'Authenticate with {provider}', + }, + openidPending: { + description: 'Waiting for authentication in your browser...', + tryAgain: 'Try again', + errorDescription: + 'There was an error during authentication. Use the try again button below to retry the authentication process.', + }, + openidUnavailable: { + description: + 'The OpenID authentication is currently unavailable. This may be due to a configuration issue or the Defguard instance is down. Please contact your administrator or try again later.', + tryAgain: 'Try again', + }, errors: { mfaNotConfigured: 'Selected method has not been configured.', mfaStartGeneric: @@ -731,6 +749,10 @@ If you want to disengage your VPN connection, simply press "deactivate". invalidCode: 'Error, this code is invalid, try again or contact your administrator.', tokenExpired: 'Token has expired. Please try to connect again.', + authenticationTimeout: + 'Authentication took too long and timed out. Please try connecting again.', + sessionInvalidated: + 'Error: Your login session might have been invalidated or expired. Please try again.', }, }, }, diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 907fe6f7..a774fea8 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -1591,6 +1591,47 @@ type RootTranslation = { * V​e​r​i​f​y */ buttonSubmit: string + openidLogin: { + /** + * I​n​ ​o​r​d​e​r​ ​t​o​ ​c​o​n​n​e​c​t​ ​t​o​ ​t​h​e​ ​V​P​N​ ​p​l​e​a​s​e​ ​l​o​g​i​n​ ​w​i​t​h​ ​{​p​r​o​v​i​d​e​r​}​.​ ​T​o​ ​d​o​ ​s​o​,​ ​p​l​e​a​s​e​ ​c​l​i​c​k​ ​"​A​u​t​h​e​n​t​i​c​a​t​e​ ​w​i​t​h​ ​{​p​r​o​v​i​d​e​r​}​"​ ​b​u​t​t​o​n​ ​b​e​l​o​w​. + * @param {unknown} provider + */ + description: RequiredParams<'provider' | 'provider'> + /** + * *​*​T​h​i​s​ ​w​i​l​l​ ​o​p​e​n​ ​a​ ​n​e​w​ ​w​i​n​d​o​w​ ​i​n​ ​y​o​u​r​ ​W​e​b​ ​B​r​o​w​s​e​r​*​*​ ​a​n​d​ ​a​u​t​o​m​a​t​i​c​a​l​l​y​ ​r​e​d​i​r​e​c​t​ ​y​o​u​ ​t​o​ ​t​h​e​ ​{​p​r​o​v​i​d​e​r​}​ ​l​o​g​i​n​ ​p​a​g​e​.​ ​A​f​t​e​r​ ​a​u​t​h​e​n​t​i​c​a​t​i​n​g​ ​w​i​t​h​ ​{​p​r​o​v​i​d​e​r​}​ ​p​l​e​a​s​e​ ​g​e​t​ ​b​a​c​k​ ​h​e​r​e​. + * @param {unknown} provider + */ + browserWarning: RequiredParams<'provider' | 'provider'> + /** + * A​u​t​h​e​n​t​i​c​a​t​e​ ​w​i​t​h​ ​{​p​r​o​v​i​d​e​r​} + * @param {unknown} provider + */ + buttonText: RequiredParams<'provider'> + } + openidPending: { + /** + * W​a​i​t​i​n​g​ ​f​o​r​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​i​n​ ​y​o​u​r​ ​b​r​o​w​s​e​r​.​.​. + */ + description: string + /** + * T​r​y​ ​a​g​a​i​n + */ + tryAgain: string + /** + * T​h​e​r​e​ ​w​a​s​ ​a​n​ ​e​r​r​o​r​ ​d​u​r​i​n​g​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​.​ ​U​s​e​ ​t​h​e​ ​t​r​y​ ​a​g​a​i​n​ ​b​u​t​t​o​n​ ​b​e​l​o​w​ ​t​o​ ​r​e​t​r​y​ ​t​h​e​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​p​r​o​c​e​s​s​. + */ + errorDescription: string + } + openidUnavailable: { + /** + * T​h​e​ ​O​p​e​n​I​D​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​i​s​ ​c​u​r​r​e​n​t​l​y​ ​u​n​a​v​a​i​l​a​b​l​e​.​ ​T​h​i​s​ ​m​a​y​ ​b​e​ ​d​u​e​ ​t​o​ ​a​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​i​s​s​u​e​ ​o​r​ ​t​h​e​ ​D​e​f​g​u​a​r​d​ ​i​n​s​t​a​n​c​e​ ​i​s​ ​d​o​w​n​.​ ​P​l​e​a​s​e​ ​c​o​n​t​a​c​t​ ​y​o​u​r​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​ ​o​r​ ​t​r​y​ ​a​g​a​i​n​ ​l​a​t​e​r​. + */ + description: string + /** + * T​r​y​ ​a​g​a​i​n + */ + tryAgain: 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​. @@ -1616,6 +1657,14 @@ type RootTranslation = { * 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 + /** + * A​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​t​o​o​k​ ​t​o​o​ ​l​o​n​g​ ​a​n​d​ ​t​i​m​e​d​ ​o​u​t​.​ ​P​l​e​a​s​e​ ​t​r​y​ ​c​o​n​n​e​c​t​i​n​g​ ​a​g​a​i​n​. + */ + authenticationTimeout: string + /** + * E​r​r​o​r​:​ ​Y​o​u​r​ ​l​o​g​i​n​ ​s​e​s​s​i​o​n​ ​m​i​g​h​t​ ​h​a​v​e​ ​b​e​e​n​ ​i​n​v​a​l​i​d​a​t​e​d​ ​o​r​ ​e​x​p​i​r​e​d​.​ ​P​l​e​a​s​e​ ​t​r​y​ ​a​g​a​i​n​. + */ + sessionInvalidated: string } } } @@ -3179,6 +3228,44 @@ export type TranslationFunctions = { * Verify */ buttonSubmit: () => LocalizedString + openidLogin: { + /** + * In order to connect to the VPN please login with {provider}. To do so, please click "Authenticate with {provider}" button below. + */ + description: (arg: { provider: unknown }) => LocalizedString + /** + * **This will open a new window in your Web Browser** and automatically redirect you to the {provider} login page. After authenticating with {provider} please get back here. + */ + browserWarning: (arg: { provider: unknown }) => LocalizedString + /** + * Authenticate with {provider} + */ + buttonText: (arg: { provider: unknown }) => LocalizedString + } + openidPending: { + /** + * Waiting for authentication in your browser... + */ + description: () => LocalizedString + /** + * Try again + */ + tryAgain: () => LocalizedString + /** + * There was an error during authentication. Use the try again button below to retry the authentication process. + */ + errorDescription: () => LocalizedString + } + openidUnavailable: { + /** + * The OpenID authentication is currently unavailable. This may be due to a configuration issue or the Defguard instance is down. Please contact your administrator or try again later. + */ + description: () => LocalizedString + /** + * Try again + */ + tryAgain: () => LocalizedString + } errors: { /** * Selected method has not been configured. @@ -3204,6 +3291,14 @@ export type TranslationFunctions = { * Token has expired. Please try to connect again. */ tokenExpired: () => LocalizedString + /** + * Authentication took too long and timed out. Please try connecting again. + */ + authenticationTimeout: () => LocalizedString + /** + * Error: Your login session might have been invalidated or expired. Please try again. + */ + sessionInvalidated: () => LocalizedString } } } diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/Icons.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/Icons.tsx new file mode 100644 index 00000000..eccb4faa --- /dev/null +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/Icons.tsx @@ -0,0 +1,212 @@ +export const GoToBrowserIcon = () => ( + + + + + + + + + + + + + + + + +); + +export const BrowserPendingIcon = () => ( + + + + + + + + + + + + + + + +); + +export const BrowserErrorIcon = () => ( + + + + + + + + + + + + + + + + +); diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx index 5193af62..b8aaca7d 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx @@ -3,9 +3,11 @@ import './style.scss'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation } from '@tanstack/react-query'; import { Body, fetch } from '@tauri-apps/api/http'; -import { useCallback, useMemo, useState } from 'react'; +import { isUndefined } from 'lodash-es'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import AuthCode from 'react-auth-code-input'; import { SubmitHandler, useForm } from 'react-hook-form'; +import ReactMarkdown from 'react-markdown'; import { error } from 'tauri-plugin-log-api'; import { z } from 'zod'; import { shallow } from 'zustand/shallow'; @@ -16,11 +18,15 @@ import { ButtonSize, ButtonStyleVariant, } from '../../../../../../../../shared/defguard-ui/components/Layout/Button/types'; +import { LoaderSpinner } from '../../../../../../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; import { MessageBox } from '../../../../../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; import { MessageBoxType } from '../../../../../../../../shared/defguard-ui/components/Layout/MessageBox/types'; import { ModalWithTitle } from '../../../../../../../../shared/defguard-ui/components/Layout/modals/ModalWithTitle/ModalWithTitle'; import { useToaster } from '../../../../../../../../shared/defguard-ui/hooks/toasts/useToaster'; import { clientApi } from '../../../../../../clientAPI/clientApi'; +import { useClientStore } from '../../../../../../hooks/useClientStore'; +import { DefguardInstance, WireguardInstanceType } from '../../../../../../types'; +import { BrowserErrorIcon, BrowserPendingIcon, GoToBrowserIcon } from './Icons'; import { useMFAModal } from './useMFAModal'; const { connect } = clientApi; @@ -44,12 +50,20 @@ type MFAStartResponse = { token: string; }; +type Screen = + | 'start' + | 'authenticator_app' + | 'email' + | 'openid_login' + | 'openid_pending' + | 'openid_unavailable'; + export const MFAModal = () => { const { LL } = useI18nContext(); const toaster = useToaster(); - const [authMethod, setAuthMethod] = useState<0 | 1>(0); - const [screen, setScreen] = useState<'start' | 'authenticator_app' | 'email'>('start'); + const [authMethod, setAuthMethod] = useState<0 | 1 | 2>(0); + const [screen, setScreen] = useState('start'); const [mfaToken, setMFAToken] = useState(''); const [proxyUrl, setProxyUrl] = useState(''); @@ -57,6 +71,20 @@ export const MFAModal = () => { const isOpen = useMFAModal((state) => state.isOpen); const location = useMFAModal((state) => state.instance); const [close, reset] = useMFAModal((state) => [state.close, state.reset], shallow); + const [selectedInstanceId, selectedInstanceType] = useClientStore((state) => [ + state.selectedInstance?.id, + state.selectedInstance?.type, + ]); + const instances = useClientStore((state) => state.instances); + const selectedInstance = useMemo((): DefguardInstance | undefined => { + if ( + !isUndefined(selectedInstanceId) && + selectedInstanceType && + selectedInstanceType === WireguardInstanceType.DEFGUARD_INSTANCE + ) { + return instances.find((i) => i.id === selectedInstanceId); + } + }, [selectedInstanceId, selectedInstanceType, instances]); const resetState = () => { reset(); @@ -69,23 +97,20 @@ export const MFAModal = () => { setMFAToken(''); }; + // selectedMethod: 0 = authenticator app, 1 = email, 2 = OpenID const startMFA = async (selectedMethod: number) => { if (!location) return toaster.error(localLL.errors.locationNotSpecified()); - const clientInstances = await clientApi.getInstances(); - const instance = clientInstances.find((i) => i.id === location.instance_id); - - if (!instance) { + if (!selectedInstance) { return toaster.error(localLL.errors.instanceNotFound()); } - setProxyUrl(instance.proxy_url + CLIENT_MFA_ENDPOINT); - const mfaStartUrl = instance.proxy_url + CLIENT_MFA_ENDPOINT + '/start'; + setProxyUrl(selectedInstance.proxy_url); + const mfaStartUrl = selectedInstance.proxy_url + CLIENT_MFA_ENDPOINT + '/start'; - // selectedMethod: 0 = authenticator app, 1 = email const data = { method: selectedMethod, - pubkey: instance.pubkey, + pubkey: selectedInstance.pubkey, location_id: location.network_id, }; @@ -100,13 +125,32 @@ export const MFAModal = () => { if (response.ok) { const { token } = response.data; - setScreen(selectedMethod === 0 ? 'authenticator_app' : 'email'); + switch (selectedMethod) { + case 0: + setScreen('authenticator_app'); + break; + case 1: + setScreen('email'); + break; + case 2: + setScreen('openid_login'); + break; + default: + toaster.error(localLL.errors.mfaStartGeneric()); + return; + } setMFAToken(token); return response.data; } else { - const error = (response.data as unknown as MFAError).error; - if (error === 'selected MFA method not available') { + const errorData = (response.data as unknown as MFAError).error; + error('MFA failed to start with the following error: ' + errorData); + if (selectedMethod === 2) { + setScreen('openid_unavailable'); + return; + } + + if (errorData === 'selected MFA method not available') { toaster.error(localLL.errors.mfaNotConfigured()); } else { toaster.error(localLL.errors.mfaStartGeneric()); @@ -116,6 +160,10 @@ export const MFAModal = () => { } }; + const useOpenIDMFA = useMemo(() => { + return selectedInstance?.use_openid_for_mfa || false; + }, [selectedInstance]); + const { mutate, isPending } = useMutation({ mutationFn: startMFA, }); @@ -130,6 +178,11 @@ export const MFAModal = () => { mutate(0); }, [mutate]); + const showOpenIDScreen = useCallback(() => { + setAuthMethod(2); + mutate(2); + }, [mutate]); + return ( { onClose={close} afterClose={resetState} > - {screen === 'start' ? ( + {useOpenIDMFA && screen === 'start' && ( + + )} + {useOpenIDMFA && screen === 'openid_unavailable' && ( + + )} + {screen === 'start' && !useOpenIDMFA && ( - ) : ( + )} + {screen === 'openid_login' && ( + + )} + {screen === 'openid_pending' && ( + + )} + {(screen === 'authenticator_app' || screen === 'email') && ( void; showEmailCodeForm: () => void; + showOpenIDScreen: () => void; +}; + +const OpenIDMFAUnavailable = ({ resetState }: { resetState: () => void }) => { + const { LL } = useI18nContext(); + const localLL = LL.modals.mfa.authentication; + + return ( +
+
+ +
+
+

{localLL.openidUnavailable.description()}

+
+
+
+
+ ); +}; + +const OpenIDMFAStart = ({ + isPending, + showOpenIDScreen, +}: { + isPending: boolean; + showOpenIDScreen: () => void; +}) => { + useEffect(() => { + if (!isPending) { + showOpenIDScreen(); + } + }, [isPending, showOpenIDScreen]); + + return ( +
+ +
+ ); }; const MFAStart = ({ @@ -220,6 +341,176 @@ type MFAFinishResponse = { preshared_key: string; }; +const OpenIDMFALogin = ({ + proxyUrl, + token, + setScreen, + openidDisplayName, +}: { + proxyUrl: string; + token: string; + openidDisplayName?: string; + resetAuthState: () => void; + setScreen: (screen: Screen) => void; +}) => { + const { LL } = useI18nContext(); + const localLL = LL.modals.mfa.authentication; + const { openLink } = clientApi; + const displayName = openidDisplayName || 'OpenID provider'; + + return ( +
+
+ +
+
+

{localLL.openidLogin.description({ provider: displayName })}

+
+ + {localLL.openidLogin.browserWarning({ provider: displayName })} + +
+
+
+
+ ); +}; + +type OpenIDMFAPendingProps = { + proxyUrl: string; + token: string; + resetState: () => void; +}; + +const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps) => { + const { LL } = useI18nContext(); + const localLL = LL.modals.mfa.authentication; + const toaster = useToaster(); + const location = useMFAModal((state) => state.instance); + const closeModal = useMFAModal((state) => state.close); + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + const TIMEOUT_DURATION = 5 * 1000 * 60; // 5 minutes timeout + // eslint-disable-next-line prefer-const + let timeoutId: NodeJS.Timeout; + + const pollMFAStatus = async () => { + if (!location) { + toaster.error(localLL.errors.mfaStartGeneric()); + setErrorMessage(localLL.errors.locationNotSpecified()); + return; + } + + const data = { token }; + const response = await fetch( + proxyUrl + CLIENT_MFA_ENDPOINT + '/finish', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: Body.json(data), + }, + ); + + if (response.ok) { + clearInterval(interval); + clearTimeout(timeoutId); + closeModal(); + + await connect({ + locationId: location?.id, + connectionType: location.connection_type, + presharedKey: response.data.preshared_key, + }); + return; + } + + // HTTP 428: Precondition required, continue waiting, the user may have not completed the OpenID login yet + if (response.status === 428) { + return; + } + + // Other errors: stop polling and handle + clearInterval(interval); + clearTimeout(timeoutId); + const { error: errorMessage } = response.data as unknown as MFAError; + + if (errorMessage === 'invalid token') { + error(JSON.stringify(response.data, null, 2)); + setErrorMessage(localLL.errors.tokenExpired()); + } else if (errorMessage === 'login session not found') { + error(JSON.stringify(response.data, null, 2)); + setErrorMessage(localLL.errors.sessionInvalidated()); + } else { + error(JSON.stringify(response.data, null, 2)); + setErrorMessage(localLL.errors.mfaStartGeneric()); + } + }; + + const handleTimeout = () => { + clearInterval(interval); + clearTimeout(timeoutId); + setErrorMessage(localLL.errors.authenticationTimeout()); + }; + + const interval = setInterval(pollMFAStatus, 5000); + timeoutId = setTimeout(handleTimeout, TIMEOUT_DURATION); + + return () => { + clearInterval(interval); + clearTimeout(timeoutId); + }; + }, [proxyUrl, token, location, closeModal, resetState, localLL.errors, toaster]); + + return ( +
+ {!errorMessage ? ( + <> +
+
+ +
+ +
+
+

{localLL.openidPending.description()}

+
+ + ) : ( + <> +
+ +
+
+

{localLL.openidPending.errorDescription()}

+

{errorMessage}

+
+ + )} +
+
+
+ ); +}; + const MFACodeForm = ({ description, token, proxyUrl, resetState }: MFACodeForm) => { const { LL } = useI18nContext(); const toaster = useToaster(); @@ -243,13 +534,16 @@ const MFACodeForm = ({ description, token, proxyUrl, resetState }: MFACodeForm) const data = { token, code: code }; - const response = await fetch(proxyUrl + '/finish', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const response = await fetch( + proxyUrl + CLIENT_MFA_ENDPOINT + '/finish', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: Body.json(data), }, - body: Body.json(data), - }); + ); if (response.ok) { closeModal(); diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/style.scss b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/style.scss index 09288c4d..45f160a2 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/style.scss +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/style.scss @@ -7,45 +7,61 @@ } .mfa-modal-content { - padding-left: 50px; - padding-right: 50px; - padding-top: 25px; display: flex; flex-direction: column; justify-content: center; align-items: center; + gap: var(--spacing-m); + + .mfa-modal-content-icon { + position: relative; + } + + .icon-spinner { + position: absolute; + top: 50px; + left: 22.26px; + } & > .mfa-modal-content-button-container { display: flex; flex-direction: column; - margin-top: 30px; - - & > button { - margin-bottom: 40px; - padding-left: 48px; - padding-right: 48px; - width: 330px; - background-color: var(--surface-button); - - & > .text { - @include typography(app-button-l); - color: var(--text-button-tertiary); - } - } + gap: var(--spacing-s); } & > .mfa-modal-content-description { - height: 70px; text-align: center; @include typography(welcome-h2); color: var(--text-body-secondary); } + .mfa-model-error-description { + display: flex; + flex-direction: column; + gap: var(--spacing-s); + } + + .mfa-model-error-message { + @include typography(app-code); + } + + .error-details { + @include typography(app-code); + } + & > p { @include typography(welcome-h2); color: var(--text-body-secondary); } + & .mfa-modal-content-footer { + button { + display: flex; + width: 303px; + padding: 12px 48px; + } + } + & > form { display: flex; flex-direction: column; diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts index 9ab660f7..45532a2f 100644 --- a/src/pages/client/types.ts +++ b/src/pages/client/types.ts @@ -8,6 +8,8 @@ export type DefguardInstance = { active: boolean; pubkey: string; disable_all_traffic: boolean; + use_openid_for_mfa: boolean; + openid_display_name?: string; }; export type DefguardLocation = { diff --git a/src/shared/defguard-ui b/src/shared/defguard-ui index d5216800..a55ca3bc 160000 --- a/src/shared/defguard-ui +++ b/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit d52168005b2ad6e3616e60c6a78df9c01746e702 +Subproject commit a55ca3bca4a5e722e4eaef0548e376bbe57e9867