diff --git a/package.json b/package.json index 69a0b08..401a4e3 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,10 @@ "@tanstack/react-query": "5.76.0", "@tanstack/react-router": "1.120.3", "@tauri-apps/api": "2.5.0", + "@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-dialog": "2.2.1", "@tauri-apps/plugin-http": "2.4.3", + "@tauri-apps/plugin-opener": "~2", "@tauri-apps/plugin-updater": "2.7.1", "framer-motion": "11.14.4", "markdown-to-jsx": "7.7.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d570df..e246f4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@auth0/auth0-react': + specifier: 2.3.0 + version: 2.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@chakra-ui/icons': specifier: 2.2.6 version: 2.2.6(@chakra-ui/react@2.8.2(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(framer-motion@11.14.4(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -32,12 +35,18 @@ importers: '@tauri-apps/api': specifier: 2.5.0 version: 2.5.0 + '@tauri-apps/plugin-deep-link': + specifier: ~2 + version: 2.2.1 '@tauri-apps/plugin-dialog': specifier: 2.2.1 version: 2.2.1 '@tauri-apps/plugin-http': specifier: 2.4.3 version: 2.4.3 + '@tauri-apps/plugin-opener': + specifier: ~2 + version: 2.2.6 '@tauri-apps/plugin-updater': specifier: 2.7.1 version: 2.7.1 @@ -100,6 +109,15 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@auth0/auth0-react@2.3.0': + resolution: {integrity: sha512-YYTc/DWWigKC9fURufR/79h3+3DAnIzbfEzJLZ8Z4Q0BXE0azru3pKUbU+vYzS4lMAJkclwLuAbUnLjK81vCpA==} + peerDependencies: + react: ^16.11.0 || ^17 || ^18 || ^19 + react-dom: ^16.11.0 || ^17 || ^18 || ^19 + + '@auth0/auth0-spa-js@2.1.3': + resolution: {integrity: sha512-NMTBNuuG4g3rame1aCnNS5qFYIzsTUV5qTFPRfTyYFS1feS6jsCBR+eTq9YkxCp1yuoM2UIcjunPaoPl77U9xQ==} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -1449,12 +1467,18 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-deep-link@2.2.1': + resolution: {integrity: sha512-8skZ6qIH/kWaV8d6jj3aPvvkIOuqkVk0APRDey9n9N3Ueu3n4MIbuxpAKR2EdoAyQxnXxPTNVyjw2D35/vfGyg==} + '@tauri-apps/plugin-dialog@2.2.1': resolution: {integrity: sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==} '@tauri-apps/plugin-http@2.4.3': resolution: {integrity: sha512-Us8X+FikzpaZRNr4kH4HLwyXascHbM42p6LxAqRTQnHPrrqp1usaH4vxWAZalPvTbHJ3gBEMJPHusFJgtjGJjA==} + '@tauri-apps/plugin-opener@2.2.6': + resolution: {integrity: sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==} + '@tauri-apps/plugin-updater@2.7.1': resolution: {integrity: sha512-1OPqEY/z7NDVSeTEMIhD2ss/vXWdpfZ5Th2Mk0KtPR/RA6FKuOTDGZQhxoyYBk0pcZJ+nNZUbl/IujDCLBApjA==} @@ -2220,6 +2244,14 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@auth0/auth0-react@2.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@auth0/auth0-spa-js': 2.1.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@auth0/auth0-spa-js@2.1.3': {} + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -3751,6 +3783,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.4.0 '@tauri-apps/cli-win32-x64-msvc': 2.4.0 + '@tauri-apps/plugin-deep-link@2.2.1': + dependencies: + '@tauri-apps/api': 2.5.0 + '@tauri-apps/plugin-dialog@2.2.1': dependencies: '@tauri-apps/api': 2.5.0 @@ -3759,6 +3795,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.5.0 + '@tauri-apps/plugin-opener@2.2.6': + dependencies: + '@tauri-apps/api': 2.5.0 + '@tauri-apps/plugin-updater@2.7.1': dependencies: '@tauri-apps/api': 2.5.0 diff --git a/public/mocks/latest.json b/public/mocks/latest.json index f37e5da..e2c9237 100644 --- a/public/mocks/latest.json +++ b/public/mocks/latest.json @@ -1,5 +1,5 @@ { - "version": "0.1.0", + "version": "0.4.1", "notes": "A test update.", "pub_date": "2024-11-12T12:00:00+00:00", "platforms": { @@ -8,4 +8,4 @@ "url": "http://localhost:1420" } } -} +} \ No newline at end of file diff --git a/sample.env b/sample.env index 556cf31..d12461e 100644 --- a/sample.env +++ b/sample.env @@ -8,3 +8,7 @@ AZURE_TENANT_ID=azure-tenant-id AZURE_CERTIFICATE_URI=https://prd-exam-environment.vault.azure.net/certificates/prd-exam-environment AZURE_CLIENT_SECRET=azure-client-secret AZURE_CLIENT_ID=azure-client-id + +VITE_AUTH0_DOMAIN=auth.freecodecamp.dev +VITE_AUTH0_CLIENT_ID="" +VITE_AUTH0_REDIRECT_URI=exam-environment://callback diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 57ca6d0..c0f4406 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -272,6 +272,92 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 0.38.44", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.38.44", + "tracing", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -283,6 +369,30 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.44", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.88" @@ -416,6 +526,19 @@ dependencies = [ "objc2 0.6.0", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" version = "7.0.0" @@ -651,6 +774,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +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.15", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -1101,6 +1244,15 @@ dependencies = [ "syn 2.0.101", ] +[[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 = "document-features" version = "0.2.11" @@ -1294,8 +1446,11 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-http", + "tauri-plugin-opener", + "tauri-plugin-single-instance", "tauri-plugin-updater", "typify", ] @@ -1894,6 +2049,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.2" @@ -1923,6 +2084,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -2323,7 +2490,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] @@ -2334,6 +2501,25 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itoa" version = "0.4.8" @@ -2585,6 +2771,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.3" @@ -3120,6 +3312,18 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.71" @@ -3170,6 +3374,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3259,6 +3473,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3411,6 +3631,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -3443,6 +3674,21 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3912,7 +4158,7 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots", - "windows-registry", + "windows-registry 0.4.0", ] [[package]] @@ -3954,6 +4200,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3975,6 +4232,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.3" @@ -3984,7 +4254,7 @@ dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.3", "windows-sys 0.59.0", ] @@ -4941,6 +5211,26 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba4412f30eaff6f5d210e20383c2d6835593977402092e95b72497a4f8632fa" +dependencies = [ + "dunce", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.12", + "tracing", + "url", + "windows-registry 0.5.1", + "windows-result", +] + [[package]] name = "tauri-plugin-dialog" version = "2.2.1" @@ -5006,6 +5296,44 @@ dependencies = [ "urlpattern", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fdc6cb608e04b7d2b6d1f21e9444ad49245f6d03465ba53323d692d1ceb1a30" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "open", + "schemars", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "url", + "windows 0.60.0", + "zbus", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1320af4d866a7fb5f5721d299d14d0dd9e4e6bc0359ff3e263124a2bf6814efa" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-plugin-deep-link", + "thiserror 2.0.12", + "tracing", + "windows-sys 0.59.0", + "zbus", +] + [[package]] name = "tauri-plugin-updater" version = "2.7.1" @@ -5144,7 +5472,7 @@ dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", - "rustix", + "rustix 1.0.3", "windows-sys 0.59.0", ] @@ -5247,6 +5575,15 @@ 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 = "tinystr" version = "0.7.6" @@ -5482,6 +5819,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" @@ -6030,7 +6373,7 @@ dependencies = [ "webview2-com-sys", "windows 0.61.1", "windows-core 0.61.0", - "windows-implement", + "windows-implement 0.60.0", "windows-interface", ] @@ -6134,17 +6477,39 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +dependencies = [ + "windows-collections 0.1.1", + "windows-core 0.60.1", + "windows-future 0.1.1", + "windows-link", + "windows-numerics 0.1.1", +] + [[package]] name = "windows" version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.0", - "windows-future", + "windows-future 0.2.0", "windows-link", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows-collections" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +dependencies = [ + "windows-core 0.60.1", ] [[package]] @@ -6174,19 +6539,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" +dependencies = [ + "windows-implement 0.59.0", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.3.1", +] + [[package]] name = "windows-core" version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement", + "windows-implement 0.60.0", "windows-interface", "windows-link", "windows-result", "windows-strings 0.4.0", ] +[[package]] +name = "windows-future" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] + [[package]] name = "windows-future" version = "0.2.0" @@ -6197,6 +6585,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -6225,6 +6624,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-numerics" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] + [[package]] name = "windows-numerics" version = "0.2.0" @@ -6246,6 +6655,17 @@ dependencies = [ "windows-targets 0.53.0", ] +[[package]] +name = "windows-registry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings 0.4.0", +] + [[package]] name = "windows-result" version = "0.3.2" @@ -6690,7 +7110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix", + "rustix 1.0.3", ] [[package]] @@ -6745,8 +7165,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" dependencies = [ "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", "async-recursion", + "async-task", "async-trait", + "blocking", "enumflags2", "event-listener", "futures-core", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 981db10..65d3ec2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,6 +29,11 @@ keyring = { version = "3.6.2", features = [ ] } tauri-plugin-http = { version = "2.4.3", features = ["multipart"] } base64 = "0.22.1" +tauri-plugin-deep-link = "2" +tauri-plugin-opener = "2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2.7.1" + +[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies] +tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index ecd9e1d..8642f80 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -30,6 +30,23 @@ }, { "url": "https://*.gitpod.io" + }, + { + "url": "https://freecodecamp-dev.auth0.com" + }, + { + "url": "https://auth.freecodecamp.dev" + } + ] + }, + { + "identifier": "opener:allow-open-url", + "allow": [ + { + "url": "https://freecodecamp-dev.auth0.com/*" + }, + { + "url": "https://*.freecodecamp.dev/*" } ] } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1a21fae..2ade096 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,8 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use tauri::{Emitter, Manager}; +use tauri_plugin_deep_link::DeepLinkExt; use utils::valid_sentry_dsn; mod commands; @@ -33,9 +35,25 @@ fn main() { let sentry_state = SentryState { client: guard }; tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_deep_link::init()) + // Ensure only one window of the app may be open at a time. + .plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| { + // NOTE: `argv` is ordinarily double-checked for CSRF for runtime-registered deep links. + // However, deep links are only registered during runtime for development. + println!("a new app instance was opened with {argv:?} and the deep link event was already triggered"); + // If app is already open, focus window when deep link is triggered + let _ = app.get_webview_window("main") + .expect("no main window") + .set_focus(); + let callback_url = argv.get(1) + .expect("no callback URL") + .to_string(); + app.emit("auth0-redirect", callback_url).expect("failed to emit deep link event"); + })) .invoke_handler(tauri::generate_handler![ commands::get_authorization_token, commands::set_authorization_token, @@ -45,6 +63,14 @@ fn main() { commands::emit_to_sentry ]) .manage(sentry_state) + .setup(|app| { + // Deep Link for app is registered during runtime as well as install, + // because this is the only way to use deep links during development. + #[cfg(desktop)] + #[cfg(debug_assertions)] + app.deep_link().register("exam-environment")?; + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/request.rs b/src-tauri/src/request.rs index 74fa697..10f46d9 100644 --- a/src-tauri/src/request.rs +++ b/src-tauri/src/request.rs @@ -25,7 +25,7 @@ pub async fn post_screenshot(image: Vec) -> Result<(), Error> { // TODO: Consider passing Response 4XX/5XX to client let _res = post - .header("Exam-Environment-Authorization-Token", authorization_token) + .header("Authorization", format!("Bearer {authorization_token}")) .multipart(form) .send() .await diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 36b9032..44c736e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -55,6 +55,13 @@ "endpoints": [ "https://github.com/freeCodeCamp/exam-env/releases/latest/download/latest.json" ] + }, + "deep-link": { + "desktop": { + "schemes": [ + "exam-environment" + ] + } } } } \ No newline at end of file diff --git a/src-tauri/tauri.dev.conf.json b/src-tauri/tauri.dev.conf.json index 9c5c805..eee20b6 100644 --- a/src-tauri/tauri.dev.conf.json +++ b/src-tauri/tauri.dev.conf.json @@ -41,6 +41,13 @@ "endpoints": [ "http://localhost:1420/mocks/latest.json" ] + }, + "deep-link": { + "desktop": { + "schemes": [ + "exam-environment" + ] + } } } } \ No newline at end of file diff --git a/src/contexts/auth.tsx b/src/contexts/auth.tsx index 38d5948..a19ab0c 100644 --- a/src/contexts/auth.tsx +++ b/src/contexts/auth.tsx @@ -4,55 +4,51 @@ import { invoke } from "@tauri-apps/api/core"; import { verifyToken } from "../utils/fetch"; export const AuthContext = createContext<{ - examEnvironmentAuthenticationToken: string | null; + accessToken: string | null; login: (token: string) => Promise; logout: () => void; } | null>(null); export const AuthProvider = ({ children }: { children: React.ReactNode }) => { - const [ - examEnvironmentAuthenticationToken, - setExamEnvironmentAuthenticationToken, - ] = useState(null); + const [accessToken, setAccessToken] = useState(null); useEffect(() => { (async () => { try { - const token = await invoke("get_authorization_token"); + const token = await invoke("get_access_token"); // If token exists in key storage, try login if (token) { await login(token); } } catch (e) { - setExamEnvironmentAuthenticationToken(null); + setAccessToken(null); } })(); }, []); const login = async (token: string) => { const res = await verifyToken(token); - // TODO: Add check that token will not expire soon - // If it will, tell user if (res.data) { - setExamEnvironmentAuthenticationToken(token); + setAccessToken(token); } else { - setExamEnvironmentAuthenticationToken(null); + setAccessToken(null); throw new Error(res.error.message); } }; const logout = async () => { - await invoke("remove_authorization_token"); - setExamEnvironmentAuthenticationToken(null); + // TODO: Invalidate in Auth0 + await invoke("remove_access_token"); + setAccessToken(null); }; const value = useMemo( () => ({ - examEnvironmentAuthenticationToken, + accessToken, login, logout, }), - [examEnvironmentAuthenticationToken] + [accessToken] ); return {children}; }; diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 31ae8ac..bbb1f07 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -1,15 +1,6 @@ -import { - Box, - Center, - Flex, - FormControl, - FormErrorMessage, - FormHelperText, - FormLabel, - Heading, - Input, -} from "@chakra-ui/react"; -import { ChangeEvent, useContext, useEffect, useState } from "react"; +import { Box, Center, Flex, Heading, Text } from "@chakra-ui/react"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { useContext, useEffect, useState } from "react"; import { createRoute, useNavigate } from "@tanstack/react-router"; import { Button, Spacer } from "@freecodecamp/ui"; @@ -18,14 +9,12 @@ import { AuthContext } from "../contexts/auth"; import { Header } from "../components/header"; import { rootRoute } from "./root"; import { LandingRoute } from "./landing"; +import { listen } from "@tauri-apps/api/event"; +import { fetch } from "@tauri-apps/plugin-http"; export function Login() { const navigate = useNavigate(); - const { login, examEnvironmentAuthenticationToken } = - useContext(AuthContext)!; - const [accountToken, setAccountToken] = useState( - examEnvironmentAuthenticationToken || "" - ); + const { login, accessToken } = useContext(AuthContext)!; const [error, setError] = useState(null); const [setAuthToken, isPending, setAuthTokenError] = useInvoke( "set_authorization_token" @@ -36,27 +25,120 @@ export function Login() { }, [setAuthTokenError]); useEffect(() => { - if (examEnvironmentAuthenticationToken) { + if (accessToken) { navigate({ to: LandingRoute.to }); } - }, [examEnvironmentAuthenticationToken]); - - function handleTokenChange(e: ChangeEvent) { - setAccountToken(e.target.value); - } + }, [accessToken]); - async function connectAuthToken() { - await setAuthToken({ - newAuthorizationToken: accountToken, - }); + // 1. Generate code_challenge + // 2. /authorize with code_challenge, code_challenge_method, audience + // - https://auth0.com/docs/api/authentication/authorization-code-flow-with-pkce/authorize-with-pkce + // 3. /oauth/token with code_verifier, redirect_uri, client_id, code + // - https://auth0.com/docs/api/authentication/authorization-code-flow-with-pkce/get-token-pkce + async function logIn() { try { - await login(accountToken); - navigate({ to: LandingRoute.to }); + const client_id = import.meta.env.VITE_AUTH0_CLIENT_ID; + const redirect_uri = import.meta.env.VITE_AUTH0_REDIRECT_URI; + const scope = "openid profile email"; + const response_type = "code"; + const code_challenge_method = "S256"; + const code_verifier = createCodeVerifier(); + const code_challenge = await createCodeChallenge(code_verifier); + + const unlisten = await listen("auth0-redirect", async (event) => { + const url = new URL(event.payload); + const code = url.searchParams.get("code"); + + const tokenUrl = new URL( + "/oauth/token", + import.meta.env.VITE_AUTH0_DOMAIN + ); + + const response = await fetch(tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + client_id, + redirect_uri, + grant_type: "authorization_code", + code_verifier, + // Safety: `code` is known to exist + // TODO: On backend, check `code` exists + code: code!, + }), + }); + + unlisten(); + if (!response.ok) { + console.error(response); + console.log(await response.json()); + throw new Error("Failed to get token"); + } + + const data = await response.json(); + + const accessToken = data.access_token; + + await setAuthToken({ + newAuthToken: accessToken, + }); + try { + await login(accessToken); + navigate({ to: LandingRoute.to }); + } catch (e) { + setError(String(e)); + } + }); + + const authorizeUrl = new URL( + "/authorize", + import.meta.env.VITE_AUTH0_DOMAIN + ); + authorizeUrl.searchParams.set("client_id", client_id); + authorizeUrl.searchParams.set("redirect_uri", redirect_uri); + // Not required, but auth0's sdk uses it + authorizeUrl.searchParams.set("scope", scope); + authorizeUrl.searchParams.set("response_type", response_type); + authorizeUrl.searchParams.set("code_challenge", code_challenge); + authorizeUrl.searchParams.set( + "code_challenge_method", + code_challenge_method + ); + await openUrl(authorizeUrl.toString()); } catch (e) { setError(String(e)); } } + function createCodeVerifier() { + const array = new Uint32Array(32); + window.crypto.getRandomValues(array); + // The octet sequence is then base64url-encoded to produce a + // 43-octet URL safe string to use as the code verifier. + const base64String = btoa(String.fromCharCode(...new Uint8Array(array))); + return base64String + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); + } + + /** + * code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) + */ + async function createCodeChallenge(codeVerifier: string) { + const encoder = new TextEncoder(); + const data = encoder.encode(codeVerifier); + const digest = await window.crypto.subtle.digest("SHA-256", data); + const base64String = btoa(String.fromCharCode(...new Uint8Array(digest))); + return base64String + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); + } + return ( <>
@@ -66,54 +148,10 @@ export function Login() { Log In - - - {" "} - Connect your freeCodeCamp.org account by inputing your account - token: - - - {!!error && ( - {JSON.stringify(error)} - )} - - Go to https://freecodecamp.org/settings to generate a token if - you do not already have one. - - - - - -
- How do I generate a token? - - -
+ + {!!error && {JSON.stringify(error)}} diff --git a/src/pages/splashscreen.tsx b/src/pages/splashscreen.tsx index b205e67..931c1bf 100644 --- a/src/pages/splashscreen.tsx +++ b/src/pages/splashscreen.tsx @@ -54,7 +54,7 @@ export function Splashscreen() { const update = updateQuery.data; const downloadAndInstallQuery = useQuery({ queryKey: ["downloadAndInstall", [update, isStartDownload]], - enabled: update?.available && isStartDownload, + enabled: !!update && isStartDownload, queryFn: async () => { let downloaded = 0; let contentLength: number | undefined = 0; @@ -124,7 +124,7 @@ export function Splashscreen() { ); } - if (update?.available && !isStartDownload) { + if (!!update && !isStartDownload) { return ( @@ -142,7 +142,7 @@ export function Splashscreen() { ); } - if (update?.available && downloadAndInstallQuery.isPending) { + if (!!update && downloadAndInstallQuery.isPending) { return ( @@ -160,7 +160,7 @@ export function Splashscreen() { ); } - if (update?.available && downloadAndInstallQuery.isError) { + if (!!update && downloadAndInstallQuery.isError) { return ( @@ -247,7 +247,7 @@ async function checkForUpdate() { interface UpdateMetadata { rid: number; - available: boolean; + available: true; currentVersion: string; version: string; date?: string; @@ -297,12 +297,13 @@ async function checkForUpdate() { return new Promise((res) => res()); } } + // Comment out to test update functionality + return null; return new MockUpdate({ rid: 0, - // Change to `true` to test the download and restart - available: false, - currentVersion: "0.0.1", - version: "0.0.2", + available: true, + currentVersion: "0.4.1", + version: "0.4.1", date: new Date().toUTCString(), body: "New update", rawJson: {}, @@ -325,6 +326,7 @@ async function checkForUpdate() { // Check device compatibility async function checkDeviceCompatibility() { + return null; if (import.meta.env.VITE_MOCK_DATA === "true") { await delayForTesting(1000); } diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index 6d9959e..8321f4a 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -29,7 +29,7 @@ export async function verifyToken(token: string) { const res = await client.GET("/exam-environment/token-meta", { params: { header: { - "exam-environment-authorization-token": token, + authorization: `Bearer ${token}`, }, }, }); @@ -63,7 +63,7 @@ export async function getGeneratedExam(examId: string) { body: { examId }, params: { header: { - "exam-environment-authorization-token": token, + authorization: `Bearer ${token}`, }, }, }); @@ -87,7 +87,7 @@ export async function postExamAttempt(examAttempt: UserExamAttempt) { body: { attempt: examAttempt }, params: { header: { - "exam-environment-authorization-token": token, + authorization: `Bearer ${token}`, }, }, }); @@ -126,7 +126,7 @@ export async function getExams() { const res = await client.GET("/exam-environment/exams", { params: { header: { - "exam-environment-authorization-token": token, + authorization: `Bearer ${token}`, }, }, });