Switch to official launcher auth (#1118)

* Switch to official launcher auth

* add debug info

* Fix build
This commit is contained in:
Geometrically 2024-04-15 13:58:20 -07:00 committed by GitHub
parent 76447019c0
commit 2877919639
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 1674 additions and 5349 deletions

View File

@ -1,44 +0,0 @@
name: CLI Build + Lint
on:
push:
branches: [ master ]
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./theseus_cli
steps:
- name: Checkout
uses: actions/checkout@v3
- name: install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- uses: actions-rs/cargo@v1
name: Build program
with:
command: build
args: --bin theseus_cli
- name: Run Lint
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --bin theseus_cli

1
.gitignore vendored
View File

@ -100,6 +100,7 @@ fabric.properties
# that are supposed to be shared within teams. # that are supposed to be shared within teams.
.idea/* .idea/*
.vscode/*
!.idea/codeStyles !.idea/codeStyles
!.idea/runConfigurations !.idea/runConfigurations

View File

@ -1,12 +0,0 @@
{
"recommendations": [
"tauri-apps.tauri-vscode",
"vunguyentuan.vscode-css-variables",
"stylelint.vscode-stylelint",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"pivaszbs.svelte-autoimport",
"svelte.svelte-vscode",
"ardenivanov.svelte-intellisense"
]
}

160
.vscode/launch.json vendored
View File

@ -1,160 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'theseus'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=theseus"
],
"filter": {
"name": "theseus",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'theseus_cli'",
"cargo": {
"args": [
"build",
"--bin=theseus_cli",
"--package=theseus_cli"
],
"filter": {
"name": "theseus_cli",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'theseus_cli'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=theseus_cli",
"--package=theseus_cli"
],
"filter": {
"name": "theseus_cli",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'theseus_playground'",
"cargo": {
"args": [
"build",
"--bin=theseus_playground",
"--package=theseus_playground"
],
"filter": {
"name": "theseus_playground",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'theseus_playground'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=theseus_playground",
"--package=theseus_playground"
],
"filter": {
"name": "theseus_playground",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'theseus_gui'",
"cargo": {
"args": [
"build",
"--bin=theseus_gui",
"--package=theseus_gui"
],
"filter": {
"name": "theseus_gui",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'theseus_gui'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=theseus_gui",
"--package=theseus_gui"
],
"filter": {
"name": "theseus_gui",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Tauri Development Debug",
"cargo": {
"args": [
"build",
"--manifest-path=./theseus_gui/src-tauri/Cargo.toml",
"--no-default-features"
]
},
"preLaunchTask": "ui:dev"
},
{
"type": "lldb",
"request": "launch",
"name": "Tauri Production Debug",
"cargo": {
"args": ["build", "--release", "--manifest-path=.theseus_gui/src-tauri/Cargo.toml"]
},
"preLaunchTask": "ui:build"
}
]
}

60
.vscode/settings.json vendored
View File

@ -1,60 +0,0 @@
{
"cssVariables.lookupFiles": [
"**/*.postcss",
"**/node_modules/omorphia/**/*.postcss"
],
"cssVariables.blacklistFolders": [
"**/.git",
"**/.svn",
"**/.hg",
"**/CVS",
"**/.DS_Store",
"**/.git",
"**/bower_components",
"**/tmp",
"**/dist",
"**/tests"
],
"gitlens.showWelcomeOnInstall": false,
"gitlens.showWhatsNewAfterUpgrades": false,
"gitlens.plusFeatures.enabled": false,
"gitlens.currentLine.enabled": false,
"gitlens.currentLine.pullRequests.enabled": false,
"gitlens.currentLine.scrollable": true,
"gitlens.codeLens.enabled": false,
"gitlens.hovers.enabled": false,
"CSSNavigation.activeCSSFileExtensions": [
"css",
"postcss"
],
"CSSNavigation.activeHTMLFileExtensions": [
"html",
"svelte",
"js",
"ts"
],
"CSSNavigation.excludeGlobPatterns": [
"**/bower_components/**",
"**/vendor/**",
"**/coverage/**"
],
"CSSNavigation.alwaysIncludeGlobPatterns": [
"./theseus_gui/node_modules/omorphia/**/*.postcss"
],
"html-css-class-completion.HTMLLanguages": [
"html",
"svelte"
],
"html-css-class-completion.includeGlobPattern": "**/*.{postcss,svelte}",
"html-css-class-completion.CSSLanguages": [
"postcss",
],
"svelte.enable-ts-plugin": true,
"svelte.ask-to-enable-ts-plugin": false,
"svelte.plugin.css.diagnostics.enable": false,
"svelte.plugin.svelte.diagnostics.enable": false,
"rust-analyzer.linkedProjects": [
"./theseus/Cargo.toml"
],
"rust-analyzer.showUnlinkedFileNotification": false,
}

32
.vscode/tasks.json vendored
View File

@ -1,32 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "ui:dev",
"type": "shell",
// `dev` keeps running in the background
// ideally you should also configure a `problemMatcher`
// see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson
"isBackground": true,
// change this to your `beforeDevCommand`:
"command": "yarn",
"args": ["dev"],
"options": {
"cwd": "${workspaceFolder}/theseus_gui"
}
},
{
"label": "ui:build",
"type": "shell",
// change this to your `beforeBuildCommand`:
"command": "yarn",
"args": ["build"],
"options": {
"cwd": "${workspaceFolder}/theseus_gui"
}
}
]
}

453
Cargo.lock generated
View File

@ -69,40 +69,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.81" version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "argh"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219"
dependencies = [
"argh_derive",
"argh_shared",
]
[[package]]
name = "argh_derive"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a"
dependencies = [
"argh_shared",
"proc-macro2",
"quote",
"syn 2.0.58",
]
[[package]]
name = "argh_shared"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
@ -147,9 +116,9 @@ dependencies = [
[[package]] [[package]]
name = "async-executor" name = "async-executor"
version = "1.9.1" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10b3e585719c2358d2660232671ca8ca4ddb4be4ce8a1842d6c2dc8685303316" checksum = "5f98c37cf288e302c16ef6c8472aad1e034c6c84ce5ea7b8101c98eb4a802fee"
dependencies = [ dependencies = [
"async-lock 3.3.0", "async-lock 3.3.0",
"async-task", "async-task",
@ -284,9 +253,9 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.79" version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -376,6 +345,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.1" version = "0.13.1"
@ -504,15 +479,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.15.4" version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytecount"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
@ -592,9 +561,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.90" version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -628,9 +597,9 @@ dependencies = [
[[package]] [[package]]
name = "cfg-expr" name = "cfg-expr"
version = "0.15.7" version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [ dependencies = [
"smallvec", "smallvec",
"target-lexicon", "target-lexicon",
@ -719,33 +688,6 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "color-eyre"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@ -754,9 +696,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.6" version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [ dependencies = [
"bytes", "bytes",
"memchr", "memchr",
@ -784,6 +726,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.1.5" version = "0.1.5"
@ -925,6 +873,18 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -1046,6 +1006,17 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -1080,19 +1051,6 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "dialoguer"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
dependencies = [
"console",
"shell-words",
"tempfile",
"thiserror",
"zeroize",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -1100,6 +1058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"const-oid",
"crypto-common", "crypto-common",
"subtle", "subtle",
] ]
@ -1185,12 +1144,46 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest",
"elliptic-curve",
"rfc6979",
"signature",
"spki",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.10.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest",
"ff",
"generic-array",
"group",
"pem-rfc7468",
"pkcs8",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "embed-resource" name = "embed-resource"
version = "2.4.2" version = "2.4.2"
@ -1219,9 +1212,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -1322,16 +1315,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.9.0" version = "1.9.0"
@ -1356,6 +1339,16 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "ff"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"
@ -1709,6 +1702,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
"zeroize",
] ]
[[package]] [[package]]
@ -1724,9 +1718,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.13" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@ -1855,6 +1849,17 @@ dependencies = [
"scroll", "scroll",
] ]
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle",
]
[[package]] [[package]]
name = "gtk" name = "gtk"
version = "0.15.5" version = "0.15.5"
@ -2280,12 +2285,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@ -2471,9 +2470,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.28" version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" checksum = "f08474e32172238f2827bd160c67871cdb2801430f65c3979184dc362e3ca118"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -3166,10 +3165,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "owo-colors" name = "p256"
version = "3.5.0" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]] [[package]]
name = "pango" name = "pango"
@ -3196,23 +3201,6 @@ dependencies = [
"system-deps 6.2.2", "system-deps 6.2.2",
] ]
[[package]]
name = "papergrid"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ad43c07024ef767f9160710b3a6773976194758c7919b17e63b863db0bdf7fb"
dependencies = [
"bytecount",
"fnv",
"unicode-width",
]
[[package]]
name = "paris"
version = "1.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fecab3723493c7851f292cb060f3ee1c42f19b8d749345d0d7eaf3fd19aa62d"
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.0" version = "2.2.0"
@ -3277,6 +3265,15 @@ dependencies = [
"sha2", "sha2",
] ]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -3460,6 +3457,16 @@ dependencies = [
"futures-io", "futures-io",
] ]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.30" version = "0.3.30"
@ -3554,6 +3561,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve",
]
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "1.3.1" version = "1.3.1"
@ -3624,9 +3640,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -3691,7 +3707,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom 0.2.13", "getrandom 0.2.14",
] ]
[[package]] [[package]]
@ -3762,7 +3778,7 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
dependencies = [ dependencies = [
"getrandom 0.2.13", "getrandom 0.2.14",
"libredox", "libredox",
"thiserror", "thiserror",
] ]
@ -3879,7 +3895,7 @@ dependencies = [
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls-pemfile 2.1.1", "rustls-pemfile 2.1.2",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
@ -3903,6 +3919,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560" checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560"
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]] [[package]]
name = "rfd" name = "rfd"
version = "0.10.0" version = "0.10.0"
@ -3980,11 +4006,11 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "2.1.1" version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.22.0",
"rustls-pki-types", "rustls-pki-types",
] ]
@ -3996,9 +4022,9 @@ checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.14" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
[[package]] [[package]]
name = "ryu" name = "ryu"
@ -4056,6 +4082,20 @@ dependencies = [
"syn 2.0.58", "syn 2.0.58",
] ]
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.10.0" version = "2.10.0"
@ -4272,9 +4312,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_repr" name = "serde_repr"
version = "0.1.18" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4410,12 +4450,6 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@ -4425,6 +4459,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@ -4511,6 +4555,16 @@ dependencies = [
"system-deps 5.0.0", "system-deps 5.0.0",
] ]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
@ -4623,9 +4677,9 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.30.8" version = "0.30.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b1a378e48fb3ce3a5cf04359c456c9c98ff689bcf1c1bc6e6a31f247686f275" checksum = "26d7c217777061d5a2d652aea771fb9ba98b6dade657204b08c4b9604d11555b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"core-foundation-sys", "core-foundation-sys",
@ -4676,37 +4730,13 @@ version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
dependencies = [ dependencies = [
"cfg-expr 0.15.7", "cfg-expr 0.15.8",
"heck 0.5.0", "heck 0.5.0",
"pkg-config", "pkg-config",
"toml 0.8.12", "toml 0.8.12",
"version-compare 0.2.0", "version-compare 0.2.0",
] ]
[[package]]
name = "tabled"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c998b0c8b921495196a48aabaf1901ff28be0760136e31604f7967b0792050e"
dependencies = [
"papergrid",
"tabled_derive",
"unicode-width",
]
[[package]]
name = "tabled_derive"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17"
dependencies = [
"heck 0.4.1",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "tao" name = "tao"
version = "0.16.8" version = "0.16.8"
@ -4918,7 +4948,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-single-instance" name = "tauri-plugin-single-instance"
version = "0.0.0" version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#cf832fe106cf272916cfdda63235c139dc68171a" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#69ee862fb7f9701f8fca9a5235aeec0eb7714b11"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -4932,7 +4962,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-window-state" name = "tauri-plugin-window-state"
version = "0.1.1" version = "0.1.1"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#cf832fe106cf272916cfdda63235c139dc68171a" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#69ee862fb7f9701f8fca9a5235aeec0eb7714b11"
dependencies = [ dependencies = [
"bincode 1.3.3", "bincode 1.3.3",
"bitflags 2.5.0", "bitflags 2.5.0",
@ -5054,6 +5084,8 @@ dependencies = [
"async-recursion", "async-recursion",
"async-tungstenite", "async-tungstenite",
"async_zip", "async_zip",
"base64 0.22.0",
"byteorder",
"bytes", "bytes",
"chrono", "chrono",
"daedalus", "daedalus",
@ -5066,7 +5098,9 @@ dependencies = [
"lazy_static", "lazy_static",
"notify", "notify",
"notify-debouncer-mini", "notify-debouncer-mini",
"p256",
"paste", "paste",
"rand 0.8.5",
"regex", "regex",
"reqwest 0.12.3", "reqwest 0.12.3",
"serde", "serde",
@ -5095,33 +5129,6 @@ dependencies = [
"zip", "zip",
] ]
[[package]]
name = "theseus_cli"
version = "0.6.3"
dependencies = [
"argh",
"color-eyre",
"daedalus",
"dialoguer",
"dirs",
"dunce",
"eyre",
"futures",
"paris",
"tabled",
"theseus",
"tokio",
"tokio-stream",
"tracing",
"tracing-error",
"tracing-futures",
"tracing-subscriber",
"url",
"uuid 1.8.0",
"webbrowser",
"winreg 0.52.0",
]
[[package]] [[package]]
name = "theseus_gui" name = "theseus_gui"
version = "0.6.3" version = "0.6.3"
@ -5223,9 +5230,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.34" version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa 1.0.11", "itoa 1.0.11",
@ -5244,9 +5251,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.17" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",
@ -5404,7 +5411,7 @@ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow 0.6.5", "winnow 0.6.6",
] ]
[[package]] [[package]]
@ -5490,16 +5497,6 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "tracing-futures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [
"pin-project",
"tracing",
]
[[package]] [[package]]
name = "tracing-log" name = "tracing-log"
version = "0.2.0" version = "0.2.0"
@ -5676,7 +5673,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [ dependencies = [
"getrandom 0.2.13", "getrandom 0.2.14",
] ]
[[package]] [[package]]
@ -5685,7 +5682,7 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
dependencies = [ dependencies = [
"getrandom 0.2.13", "getrandom 0.2.14",
"serde", "serde",
] ]
@ -5885,9 +5882,9 @@ dependencies = [
[[package]] [[package]]
name = "webbrowser" name = "webbrowser"
version = "0.8.14" version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd595fb70f33583ac61644820ebc144a26c96028b625b96cafcd861f4743fbc8" checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b"
dependencies = [ dependencies = [
"core-foundation", "core-foundation",
"home", "home",
@ -6419,9 +6416,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.5" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View File

@ -2,7 +2,6 @@
members = [ members = [
"theseus", "theseus",
"theseus_cli",
"theseus_playground", "theseus_playground",
"theseus_gui/src-tauri", "theseus_gui/src-tauri",
"theseus_macros" "theseus_macros"

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/theseus/library" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -35,7 +35,7 @@ sysinfo = "0.30.8"
thiserror = "1.0" thiserror = "1.0"
tracing = "0.1.37" tracing = "0.1.37"
tracing-subscriber = { version = "0.3.18", features = ["chrono"] } tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] }
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
@ -61,6 +61,11 @@ whoami = "1.4.0"
discord-rich-presence = "0.2.3" discord-rich-presence = "0.2.3"
p256 = { version = "0.13.2", features = ["ecdsa"] }
rand = "0.8"
byteorder = "1.5.0"
base64 = "0.22.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.52.0" winreg = "0.52.0"

View File

@ -1,132 +0,0 @@
//! Authentication flow interface
use crate::{
hydra::{self, init::DeviceLoginSuccess},
launcher::auth as inner,
State,
};
use chrono::Utc;
use crate::state::AuthTask;
pub use inner::Credentials;
/// Authenticate a user with Hydra - part 1
/// This begins the authentication flow quasi-synchronously, returning a URL
/// This can be used in conjunction with 'authenticate_await_complete_flow'
/// to call authenticate and call the flow from the frontend.
/// Visit the URL in a browser, then call and await 'authenticate_await_complete_flow'.
pub async fn authenticate_begin_flow() -> crate::Result<DeviceLoginSuccess> {
let url = AuthTask::begin_auth().await?;
Ok(url)
}
/// Authenticate a user with Hydra - part 2
/// This completes the authentication flow quasi-synchronously, returning the credentials
/// This can be used in conjunction with 'authenticate_begin_flow'
/// to call authenticate and call the flow from the frontend.
pub async fn authenticate_await_complete_flow() -> crate::Result<Credentials> {
let credentials = AuthTask::await_auth_completion().await?;
Ok(credentials)
}
/// Cancels the active authentication flow
pub async fn cancel_flow() -> crate::Result<()> {
AuthTask::cancel().await
}
/// Refresh some credentials using Hydra, if needed
/// This is the primary desired way to get credentials, as it will also refresh them.
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?;
let mut users = state.users.write().await;
let mut credentials = users.get(user).ok_or_else(|| {
crate::ErrorKind::OtherError(
"You are not logged in with a Minecraft account!".to_string(),
)
.as_error()
})?;
let offline = *state.offline.read().await;
if !offline {
let fetch_semaphore: &crate::util::fetch::FetchSemaphore =
&state.fetch_semaphore;
if Utc::now() > credentials.expires
&& inner::refresh_credentials(&mut credentials, fetch_semaphore)
.await
.is_err()
{
users.remove(credentials.id).await?;
return Err(crate::ErrorKind::OtherError(
"Please re-authenticate with your Minecraft account!"
.to_string(),
)
.as_error());
}
// Update player info from bearer token
let player_info =
hydra::stages::player_info::fetch_info(&credentials.access_token)
.await
.map_err(|_err| {
crate::ErrorKind::HydraError(
"No Minecraft account for your profile. Please try again or contact support in our Discord for help!".to_string(),
)
})?;
credentials.username = player_info.name;
users.insert(&credentials).await?;
}
Ok(credentials)
}
/// Remove a user account from the database
#[tracing::instrument]
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let mut users = state.users.write().await;
if state.settings.read().await.default_user == Some(user) {
let mut settings = state.settings.write().await;
settings.default_user = users.0.values().next().map(|it| it.id);
}
users.remove(user).await?;
Ok(())
}
/// Check if a user exists in Theseus
#[tracing::instrument]
pub async fn has_user(user: uuid::Uuid) -> crate::Result<bool> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.contains(user))
}
/// Get a copy of the list of all user credentials
#[tracing::instrument]
pub async fn users() -> crate::Result<Vec<Credentials>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.0.values().cloned().collect())
}
/// Get a specific user by user ID
/// Prefer to use 'refresh' instead of this function
#[tracing::instrument]
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?;
let users = state.users.read().await;
let user = users.get(user).ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to get nonexistent user with ID {user}"
))
.as_error()
})?;
Ok(user)
}

View File

@ -1,84 +0,0 @@
//! Main authentication flow for Hydra
use serde::Deserialize;
use crate::prelude::Credentials;
use super::stages::{
bearer_token, player_info, poll_response, xbl_signin, xsts_token,
};
#[derive(Debug, Deserialize)]
pub struct OauthFailure {
pub error: String,
}
pub struct SuccessfulLogin {
pub name: String,
pub icon: String,
pub token: String,
pub refresh_token: String,
pub expires_after: i64,
}
pub async fn wait_finish(device_code: String) -> crate::Result<Credentials> {
// Loop, polling for response from Microsoft
let oauth = poll_response::poll_response(device_code).await?;
// Get xbl token from oauth token
let xbl_token = xbl_signin::login_xbl(&oauth.access_token).await?;
// Get xsts token from xbl token
let xsts_response = xsts_token::fetch_token(&xbl_token.token).await?;
match xsts_response {
xsts_token::XSTSResponse::Unauthorized(err) => {
Err(crate::ErrorKind::HydraError(format!(
"Error getting XBox Live token: {}",
err
))
.as_error())
}
xsts_token::XSTSResponse::Success { token: xsts_token } => {
// Get xsts bearer token from xsts token
let (bearer_token, expires_in) =
bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs)
.await
.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Error getting bearer token: {}",
err
))
})?;
// Get player info from bearer token
let player_info = player_info::fetch_info(&bearer_token).await.map_err(|_err| {
crate::ErrorKind::HydraError("No Minecraft account for profile. Make sure you own the game and have set a username through the official Minecraft launcher."
.to_string())
})?;
// Create credentials
let credentials = Credentials::new(
uuid::Uuid::parse_str(&player_info.id)?, // get uuid from player_info.id which is a String
player_info.name,
bearer_token,
oauth.refresh_token,
chrono::Utc::now() + chrono::Duration::seconds(expires_in),
);
// Put credentials into state
let state = crate::State::get().await?;
{
let mut users = state.users.write().await;
users.insert(&credentials).await?;
}
if state.settings.read().await.default_user.is_none() {
let mut settings = state.settings.write().await;
settings.default_user = Some(credentials.id);
}
Ok(credentials)
}
}
}

View File

@ -1,47 +0,0 @@
//! Login route for Hydra, redirects to the Microsoft login page before going to the redirect route
use serde::{Deserialize, Serialize};
use crate::{hydra::MicrosoftError, util::fetch::REQWEST_CLIENT};
use super::{stages::auth_retry, MICROSOFT_CLIENT_ID};
#[derive(Serialize, Deserialize, Debug)]
pub struct DeviceLoginSuccess {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub expires_in: u64,
pub interval: u64,
pub message: String,
}
pub async fn init() -> crate::Result<DeviceLoginSuccess> {
// Get the initial URL
// Get device code
// Define the parameters
// urlencoding::encode("XboxLive.signin offline_access"));
let resp = auth_retry(|| REQWEST_CLIENT.get("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
.header("Content-Length", "0")
.query(&[
("client_id", MICROSOFT_CLIENT_ID),
(
"scope",
"XboxLive.signin XboxLive.offline_access profile openid email",
),
])
.send()
).await?;
match resp.status() {
reqwest::StatusCode::OK => Ok(resp.json().await?),
_ => {
let microsoft_error = resp.json::<MicrosoftError>().await?;
Err(crate::ErrorKind::HydraError(format!(
"Error from Microsoft: {:?}",
microsoft_error.error_description
))
.into())
}
}
}

View File

@ -1,15 +0,0 @@
pub mod complete;
pub mod init;
pub mod refresh;
pub(crate) mod stages;
use serde::Deserialize;
const MICROSOFT_CLIENT_ID: &str = "c4502edb-87c6-40cb-b595-64a280cf8906";
#[derive(Deserialize)]
pub struct MicrosoftError {
pub error: String,
pub error_description: String,
pub error_codes: Vec<u64>,
}

View File

@ -1,69 +0,0 @@
use std::collections::HashMap;
use reqwest::StatusCode;
use serde::Deserialize;
use crate::{
hydra::{MicrosoftError, MICROSOFT_CLIENT_ID},
util::fetch::REQWEST_CLIENT,
};
use super::stages::auth_retry;
#[derive(Debug, Deserialize)]
pub struct OauthSuccess {
pub token_type: String,
pub scope: String,
pub expires_in: i64,
pub access_token: String,
pub refresh_token: String,
}
pub async fn refresh(refresh_token: String) -> crate::Result<OauthSuccess> {
let mut params = HashMap::new();
params.insert("grant_type", "refresh_token");
params.insert("client_id", MICROSOFT_CLIENT_ID);
params.insert("refresh_token", &refresh_token);
params.insert(
"redirect_uri",
"https://login.microsoftonline.com/common/oauth2/nativeclient",
);
// Poll the URL in a loop until we are successful.
// On an authorization_pending response, wait 5 seconds and try again.
let resp =
auth_retry(|| {
REQWEST_CLIENT
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
.header("Content-Type", "application/x-www-form-urlencoded")
.form(&params)
.send()
})
.await?;
match resp.status() {
StatusCode::OK => {
let oauth = resp.json::<OauthSuccess>().await.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Could not decipher successful response: {}",
err
))
})?;
Ok(oauth)
}
_ => {
let failure =
resp.json::<MicrosoftError>().await.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Could not decipher failure response: {}",
err
))
})?;
Err(crate::ErrorKind::HydraError(format!(
"Error refreshing token: {}",
failure.error
))
.as_error())
}
}
}

View File

@ -1,42 +0,0 @@
use serde::Deserialize;
use serde_json::json;
use super::auth_retry;
const MCSERVICES_AUTH_URL: &str =
"https://api.minecraftservices.com/authentication/login_with_xbox";
#[derive(Deserialize)]
pub struct BearerTokenResponse {
access_token: String,
expires_in: i64,
}
#[tracing::instrument]
pub async fn fetch_bearer(
token: &str,
uhs: &str,
) -> crate::Result<(String, i64)> {
let body = auth_retry(|| {
let client = reqwest::Client::new();
client
.post(MCSERVICES_AUTH_URL)
.header("Accept", "application/json")
.json(&json!({
"identityToken": format!("XBL3.0 x={};{}", uhs, token),
}))
.send()
})
.await?
.text()
.await?;
serde_json::from_str::<BearerTokenResponse>(&body)
.map(|x| (x.access_token, x.expires_in))
.map_err(|_| {
crate::ErrorKind::HydraError(format!(
"Response didn't contain valid bearer token. body: {body}"
))
.into()
})
}

View File

@ -1,37 +0,0 @@
//! MSA authentication stages
use futures::Future;
use reqwest::Response;
const RETRY_COUNT: usize = 9; // Does command 3 times
const RETRY_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
pub mod bearer_token;
pub mod player_info;
pub mod poll_response;
pub mod xbl_signin;
pub mod xsts_token;
#[tracing::instrument(skip(reqwest_request))]
pub async fn auth_retry<F>(
reqwest_request: impl Fn() -> F,
) -> crate::Result<reqwest::Response>
where
F: Future<Output = Result<Response, reqwest::Error>>,
{
let mut resp = reqwest_request().await?;
for i in 0..RETRY_COUNT {
if resp.status().is_success() {
break;
}
tracing::debug!(
"Request failed with status code {}, retrying...",
resp.status()
);
if i < RETRY_COUNT - 1 {
tokio::time::sleep(RETRY_WAIT).await;
}
resp = reqwest_request().await?;
}
Ok(resp)
}

View File

@ -1,46 +0,0 @@
//! Fetch player info for display
use serde::Deserialize;
use crate::util::fetch::REQWEST_CLIENT;
use super::auth_retry;
const PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";
#[derive(Deserialize)]
pub struct PlayerInfo {
pub id: String,
pub name: String,
}
impl Default for PlayerInfo {
fn default() -> Self {
Self {
id: "606e2ff0ed7748429d6ce1d3321c7838".to_string(),
name: String::from("Unknown"),
}
}
}
#[tracing::instrument]
pub async fn fetch_info(token: &str) -> crate::Result<PlayerInfo> {
auth_retry(|| {
REQWEST_CLIENT
.get("https://api.minecraftservices.com/entitlements/mcstore")
.bearer_auth(token)
.send()
})
.await?;
let response = auth_retry(|| {
REQWEST_CLIENT
.get(PROFILE_URL)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
.send()
})
.await?;
let resp = response.error_for_status()?.json().await?;
Ok(resp)
}

View File

@ -1,99 +0,0 @@
use std::collections::HashMap;
use reqwest::StatusCode;
use serde::Deserialize;
use crate::{
hydra::{MicrosoftError, MICROSOFT_CLIENT_ID},
util::fetch::REQWEST_CLIENT,
};
use super::auth_retry;
#[derive(Debug, Deserialize)]
pub struct OauthSuccess {
pub token_type: String,
pub scope: String,
pub expires_in: i64,
pub access_token: String,
pub refresh_token: String,
}
#[tracing::instrument]
pub async fn poll_response(device_code: String) -> crate::Result<OauthSuccess> {
let mut params = HashMap::new();
params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
params.insert("client_id", MICROSOFT_CLIENT_ID);
params.insert("device_code", &device_code);
params.insert(
"scope",
"XboxLive.signin XboxLive.offline_access profile openid email",
);
// Poll the URL in a loop until we are successful.
// On an authorization_pending response, wait 5 seconds and try again.
loop {
let resp = auth_retry(|| {
REQWEST_CLIENT
.post(
"https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
)
.form(&params)
.send()
})
.await?;
match resp.status() {
StatusCode::OK => {
let oauth =
resp.json::<OauthSuccess>().await.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Could not decipher successful response: {}",
err
))
})?;
return Ok(oauth);
}
_ => {
let failure =
resp.json::<MicrosoftError>().await.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Could not decipher failure response: {}",
err
))
})?;
match failure.error.as_str() {
"authorization_pending" => {
tokio::time::sleep(std::time::Duration::from_secs(2))
.await;
}
"authorization_declined" => {
return Err(crate::ErrorKind::HydraError(
"Authorization declined".to_string(),
)
.as_error());
}
"expired_token" => {
return Err(crate::ErrorKind::HydraError(
"Device code expired".to_string(),
)
.as_error());
}
"bad_verification_code" => {
return Err(crate::ErrorKind::HydraError(
"Invalid device code".to_string(),
)
.as_error());
}
_ => {
return Err(crate::ErrorKind::HydraError(format!(
"Unknown error: {}",
failure.error
))
.as_error());
}
}
}
}
}
}

View File

@ -1,59 +0,0 @@
use serde_json::json;
use crate::util::fetch::REQWEST_CLIENT;
use super::auth_retry;
const XBL_AUTH_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";
// Deserialization
pub struct XBLLogin {
pub token: String,
pub uhs: String,
}
// Impl
#[tracing::instrument]
pub async fn login_xbl(token: &str) -> crate::Result<XBLLogin> {
let response = auth_retry(|| {
REQWEST_CLIENT
.post(XBL_AUTH_URL)
.header(reqwest::header::ACCEPT, "application/json")
.json(&json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": format!("d={token}")
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}))
.send()
})
.await?;
let body = response.text().await?;
let json = serde_json::from_str::<serde_json::Value>(&body)?;
let token = Some(&json)
.and_then(|it| it.get("Token")?.as_str().map(String::from))
.ok_or(crate::ErrorKind::HydraError(
"XBL response didn't contain valid token".to_string(),
))?;
let uhs = Some(&json)
.and_then(|it| {
it.get("DisplayClaims")?
.get("xui")?
.get(0)?
.get("uhs")?
.as_str()
.map(String::from)
})
.ok_or(
crate::ErrorKind::HydraError(
"XBL response didn't contain valid user hash".to_string(),
)
.as_error(),
)?;
Ok(XBLLogin { token, uhs })
}

View File

@ -1,62 +0,0 @@
use serde_json::json;
use crate::util::fetch::REQWEST_CLIENT;
use super::auth_retry;
const XSTS_AUTH_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
pub enum XSTSResponse {
Unauthorized(String),
Success { token: String },
}
#[tracing::instrument]
pub async fn fetch_token(token: &str) -> crate::Result<XSTSResponse> {
let resp = auth_retry(|| {
REQWEST_CLIENT
.post(XSTS_AUTH_URL)
.header(reqwest::header::ACCEPT, "application/json")
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
})
.await?;
let status = resp.status();
let body = resp.text().await?;
let json = serde_json::from_str::<serde_json::Value>(&body)?;
if status.is_success() {
Ok(json
.get("Token")
.and_then(|x| x.as_str().map(String::from))
.map(|it| XSTSResponse::Success { token: it })
.unwrap_or(XSTSResponse::Unauthorized(
"XSTS response didn't contain valid token!".to_string(),
)))
} else {
Ok(XSTSResponse::Unauthorized(
#[allow(clippy::unreadable_literal)]
match json.get("XErr").and_then(|x| x.as_i64()) {
Some(2148916238) => {
String::from("This Microsoft account is underage and is not linked to a family.")
},
Some(2148916235) => {
String::from("XBOX Live/Minecraft is not available in your country.")
},
Some(2148916233) => String::from("This account does not have a valid XBOX Live profile. Please buy Minecraft and try again!"),
Some(2148916236) | Some(2148916237) => String::from("This account needs adult verification on Xbox page."),
_ => String::from("Unknown error code"),
},
))
}
}

View File

@ -140,8 +140,14 @@ pub async fn get_output_by_filename(
let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?; let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?;
let path = logs_folder.join(file_name); let path = logs_folder.join(file_name);
let credentials: Vec<Credentials> = let credentials: Vec<Credentials> = state
state.users.read().await.clone().0.into_values().collect(); .users
.read()
.await
.users
.clone()
.into_values()
.collect();
// Load .gz file into String // Load .gz file into String
if let Some(ext) = path.extension() { if let Some(ext) = path.extension() {
@ -296,8 +302,14 @@ pub async fn get_generic_live_log_cursor(
let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String
let cursor = cursor + bytes_read as u64; // Update cursor let cursor = cursor + bytes_read as u64; // Update cursor
let credentials: Vec<Credentials> = let credentials: Vec<Credentials> = state
state.users.read().await.clone().0.into_values().collect(); .users
.read()
.await
.users
.clone()
.into_values()
.collect();
let output = CensoredString::censor(output, &credentials); let output = CensoredString::censor(output, &credentials);
Ok(LatestLogCursor { Ok(LatestLogCursor {
cursor, cursor,

View File

@ -0,0 +1,76 @@
//! Authentication flow interface
use crate::state::{Credentials, MinecraftLoginFlow};
use crate::State;
#[tracing::instrument]
pub async fn begin_login() -> crate::Result<MinecraftLoginFlow> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.login_begin().await
}
#[tracing::instrument]
pub async fn finish_login(
code: &str,
flow: MinecraftLoginFlow,
) -> crate::Result<Credentials> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.login_finish(code, flow).await
}
#[tracing::instrument]
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.default_user)
}
#[tracing::instrument]
pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> {
let user = get_user(user).await?;
let state = State::get().await?;
let mut users = state.users.write().await;
users.default_user = Some(user.id);
users.save().await?;
Ok(())
}
/// Remove a user account from the database
#[tracing::instrument]
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.remove(user).await?;
Ok(())
}
/// Get a copy of the list of all user credentials
#[tracing::instrument]
pub async fn users() -> crate::Result<Vec<Credentials>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.users.values().cloned().collect())
}
/// Get a specific user by user ID
/// Prefer to use 'refresh' instead of this function
#[tracing::instrument]
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?;
let users = state.users.read().await;
let user = users
.users
.get(&user)
.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to get nonexistent user with ID {user}"
))
.as_error()
})?
.clone();
Ok(user)
}

View File

@ -1,10 +1,9 @@
//! API for interacting with Theseus //! API for interacting with Theseus
pub mod auth;
pub mod handler; pub mod handler;
pub mod hydra;
pub mod jre; pub mod jre;
pub mod logs; pub mod logs;
pub mod metadata; pub mod metadata;
pub mod minecraft_auth;
pub mod mr_auth; pub mod mr_auth;
pub mod pack; pub mod pack;
pub mod process; pub mod process;
@ -15,19 +14,19 @@ pub mod tags;
pub mod data { pub mod data {
pub use crate::state::{ pub use crate::state::{
DirectoryInfo, Hooks, JavaSettings, LinkedData, MemorySettings, Credentials, DirectoryInfo, Hooks, JavaSettings, LinkedData,
ModLoader, ModrinthCredentials, ModrinthCredentialsResult, MemorySettings, ModLoader, ModrinthCredentials,
ModrinthProject, ModrinthTeamMember, ModrinthUser, ModrinthVersion, ModrinthCredentialsResult, ModrinthProject, ModrinthTeamMember,
ProfileMetadata, ProjectMetadata, Settings, Theme, WindowSize, ModrinthUser, ModrinthVersion, ProfileMetadata, ProjectMetadata,
Settings, Theme, WindowSize,
}; };
} }
pub mod prelude { pub mod prelude {
pub use crate::{ pub use crate::{
auth::{self, Credentials},
data::*, data::*,
event::CommandPayload, event::CommandPayload,
jre, metadata, pack, process, jre, metadata, minecraft_auth, pack, process,
profile::{self, create, Profile}, profile::{self, create, Profile},
settings, settings,
state::JavaGlobals, state::JavaGlobals,

View File

@ -8,12 +8,13 @@ use crate::pack::install_from::{
EnvType, PackDependency, PackFile, PackFileHash, PackFormat, EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
}; };
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId}; use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType}; use crate::state::{
Credentials, InnerProjectPathUnix, ProjectMetadata, SideType,
};
use crate::util::fetch; use crate::util::fetch;
use crate::util::io::{self, IOError}; use crate::util::io::{self, IOError};
use crate::{ use crate::{
auth::{self, refresh},
event::{emit::emit_profile, ProfilePayloadType}, event::{emit::emit_profile, ProfilePayloadType},
state::MinecraftChild, state::MinecraftChild,
}; };
@ -745,20 +746,16 @@ pub async fn run(
let state = State::get().await?; let state = State::get().await?;
// Get default account and refresh credentials (preferred way to log in) // Get default account and refresh credentials (preferred way to log in)
let default_account = state.settings.read().await.default_user; let default_account = {
let credentials = if let Some(default_account) = default_account { let mut write = state.users.write().await;
refresh(default_account).await?
} else { write
// If no default account, try to use a logged in account .get_default_credential()
let users = auth::users().await?; .await?
let last_account = users.first(); .ok_or_else(|| crate::ErrorKind::NoCredentialsError.as_error())?
if let Some(last_account) = last_account {
refresh(last_account.id).await?
} else {
return Err(crate::ErrorKind::NoCredentialsError.as_error());
}
}; };
run_credentials(path, &credentials).await
run_credentials(path, &default_account).await
} }
/// Run Minecraft using a profile, and credentials for authentication /// Run Minecraft using a profile, and credentials for authentication
@ -767,7 +764,7 @@ pub async fn run(
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn run_credentials( pub async fn run_credentials(
path: &ProfilePathId, path: &ProfilePathId,
credentials: &auth::Credentials, credentials: &Credentials,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> { ) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let state = State::get().await?; let state = State::get().await?;
let settings = state.settings.read().await; let settings = state.settings.read().await;

View File

@ -25,11 +25,10 @@ pub enum ErrorKind {
#[error("Metadata error: {0}")] #[error("Metadata error: {0}")]
MetadataError(#[from] daedalus::Error), MetadataError(#[from] daedalus::Error),
#[error("Minecraft authentication Hydra error: {0}")] #[error("Minecraft authentication error: {0}")]
HydraError(String), MinecraftAuthenticationError(
#[from] crate::state::MinecraftAuthenticationError,
#[error("Minecraft authentication task error: {0}")] ),
AuthTaskError(#[from] crate::state::AuthTaskError),
#[error("I/O error: {0}")] #[error("I/O error: {0}")]
IOError(#[from] util::io::IOError), IOError(#[from] util::io::IOError),

View File

@ -43,7 +43,7 @@ impl EventState {
})) }))
}) })
.await .await
.map(Arc::clone) .cloned()
} }
#[cfg(feature = "tauri")] #[cfg(feature = "tauri")]

View File

@ -1,6 +1,6 @@
//! Minecraft CLI argument logic //! Minecraft CLI argument logic
use super::auth::Credentials;
use crate::launcher::parse_rules; use crate::launcher::parse_rules;
use crate::state::Credentials;
use crate::{ use crate::{
state::{MemorySettings, WindowSize}, state::{MemorySettings, WindowSize},
util::{io::IOError, platform::classpath_separator}, util::{io::IOError, platform::classpath_separator},

View File

@ -1,84 +0,0 @@
//! Authentication flow based on Hydra
use crate::hydra;
use crate::util::fetch::FetchSemaphore;
use chrono::{prelude::*, Duration};
use serde::{Deserialize, Serialize};
use crate::api::hydra::stages::{bearer_token, xbl_signin, xsts_token};
// Login information
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Credentials {
pub id: uuid::Uuid,
pub username: String,
pub access_token: String,
pub refresh_token: String,
pub expires: DateTime<Utc>,
_ctor_scope: std::marker::PhantomData<()>,
}
impl Credentials {
pub fn new(
id: uuid::Uuid,
username: String,
access_token: String,
refresh_token: String,
expires: DateTime<Utc>,
) -> Self {
Self {
id,
username,
access_token,
refresh_token,
expires,
_ctor_scope: std::marker::PhantomData,
}
}
pub fn is_expired(&self) -> bool {
self.expires < Utc::now()
}
}
pub async fn refresh_credentials(
credentials: &mut Credentials,
_semaphore: &FetchSemaphore,
) -> crate::Result<()> {
let oauth =
hydra::refresh::refresh(credentials.refresh_token.clone()).await?;
let xbl_token = xbl_signin::login_xbl(&oauth.access_token).await?;
// Get xsts token from xbl token
let xsts_response = xsts_token::fetch_token(&xbl_token.token).await?;
match xsts_response {
xsts_token::XSTSResponse::Unauthorized(err) => {
return Err(crate::ErrorKind::HydraError(format!(
"Error getting XBox Live token: {}",
err
))
.as_error())
}
xsts_token::XSTSResponse::Success { token: xsts_token } => {
let (bearer_token, expires_in) =
bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs)
.await
.map_err(|err| {
crate::ErrorKind::HydraError(format!(
"Error getting bearer token: {}",
err
))
})?;
credentials.access_token = bearer_token;
credentials.refresh_token = oauth.refresh_token;
credentials.expires = Utc::now() + Duration::seconds(expires_in);
}
}
Ok(())
}

View File

@ -4,7 +4,7 @@ use crate::event::{LoadingBarId, LoadingBarType};
use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY}; use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
use crate::launcher::io::IOError; use crate::launcher::io::IOError;
use crate::prelude::JavaVersion; use crate::prelude::JavaVersion;
use crate::state::ProfileInstallStage; use crate::state::{Credentials, ProfileInstallStage};
use crate::util::io; use crate::util::io;
use crate::{ use crate::{
process, process,
@ -22,7 +22,6 @@ use uuid::Uuid;
mod args; mod args;
pub mod auth;
pub mod download; pub mod download;
// All nones -> disallowed // All nones -> disallowed
@ -368,7 +367,7 @@ pub async fn launch_minecraft(
wrapper: &Option<String>, wrapper: &Option<String>,
memory: &st::MemorySettings, memory: &st::MemorySettings,
resolution: &st::WindowSize, resolution: &st::WindowSize,
credentials: &auth::Credentials, credentials: &Credentials,
post_exit_hook: Option<String>, post_exit_hook: Option<String>,
profile: &Profile, profile: &Profile,
) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> { ) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> {

View File

@ -1,86 +0,0 @@
use crate::{
hydra::{self, init::DeviceLoginSuccess},
launcher::auth::Credentials,
};
use tokio::task::JoinHandle;
// Authentication task
// A wrapper over the authentication task that allows it to be called from the frontend
// without caching the task handle in the frontend
pub struct AuthTask(
#[allow(clippy::type_complexity)]
Option<JoinHandle<crate::Result<Credentials>>>,
);
impl AuthTask {
pub fn new() -> AuthTask {
AuthTask(None)
}
pub async fn begin_auth() -> crate::Result<DeviceLoginSuccess> {
let state = crate::State::get().await?;
// Init task, get url
let login = hydra::init::init().await?;
// Await completion
let task = tokio::spawn(hydra::complete::wait_finish(
login.device_code.clone(),
));
// Flow is going, store in state and return
let mut write = state.auth_flow.write().await;
write.0 = Some(task);
Ok(login)
}
pub async fn await_auth_completion() -> crate::Result<Credentials> {
// Gets the task handle from the state, replacing with None
let task = {
let state = crate::State::get().await?;
let mut write = state.auth_flow.write().await;
write.0.take()
};
// Waits for the task to complete, and returns the credentials
let credentials = task
.ok_or(AuthTaskError::TaskMissing)?
.await
.map_err(AuthTaskError::from)??;
Ok(credentials)
}
pub async fn cancel() -> crate::Result<()> {
// Gets the task handle from the state, replacing with None
let task = {
let state = crate::State::get().await?;
let mut write = state.auth_flow.write().await;
write.0.take()
};
if let Some(task) = task {
// Cancels the task
task.abort();
}
Ok(())
}
}
impl Default for AuthTask {
fn default() -> Self {
Self::new()
}
}
#[derive(thiserror::Error, Debug)]
pub enum AuthTaskError {
#[error("Authentication task was aborted or missing")]
TaskMissing,
#[error("Join handle error")]
JoinHandleError(#[from] tokio::task::JoinError),
}

View File

@ -0,0 +1,878 @@
use crate::data::DirectoryInfo;
use crate::util::fetch::{read_json, write, IoSemaphore, REQWEST_CLIENT};
use crate::State;
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
use base64::Engine;
use byteorder::BigEndian;
use chrono::{DateTime, Duration, Utc};
use p256::ecdsa::signature::Signer;
use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
use rand::rngs::OsRng;
use rand::Rng;
use reqwest::header::HeaderMap;
use reqwest::Response;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::future::Future;
use uuid::Uuid;
#[derive(Debug, Clone, Copy)]
pub enum MinecraftAuthStep {
GetDeviceToken,
SisuAuthenicate,
GetOAuthToken,
RefreshOAuthToken,
SisuAuthorize,
XstsAuthorize,
MinecraftToken,
MinecraftEntitlements,
MinecraftProfile,
}
#[derive(thiserror::Error, Debug)]
pub enum MinecraftAuthenticationError {
#[error("Failed to serialize private key to PEM: {0}")]
PEMSerialize(#[from] p256::pkcs8::Error),
#[error("Failed to serialize body to JSON during step {step:?}: {source}")]
SerializeBody {
step: MinecraftAuthStep,
#[source]
source: serde_json::Error,
},
#[error(
"Failed to deserialize response to JSON during step {step:?}: {source}"
)]
DeserializeResponse {
step: MinecraftAuthStep,
raw: String,
#[source]
source: serde_json::Error,
},
#[error("Request failed during step {step:?}: {source}")]
Request {
step: MinecraftAuthStep,
#[source]
source: reqwest::Error,
},
#[error("Error creating signed request buffer {step:?}: {source}")]
ConstructingSignedRequest {
step: MinecraftAuthStep,
#[source]
source: std::io::Error,
},
#[error("Error reading user hash")]
NoUserHash,
}
const AUTH_JSON: &str = "minecraft_auth.json";
#[derive(Serialize, Deserialize, Debug)]
pub struct SaveDeviceToken {
pub id: String,
pub private_key: String,
pub x: String,
pub y: String,
pub token: DeviceToken,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MinecraftLoginFlow {
pub challenge: String,
pub session_id: String,
pub redirect_uri: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MinecraftAuthStore {
pub users: HashMap<Uuid, Credentials>,
pub token: Option<SaveDeviceToken>,
pub default_user: Option<Uuid>,
}
impl MinecraftAuthStore {
#[tracing::instrument]
pub async fn init(
dirs: &DirectoryInfo,
io_semaphore: &IoSemaphore,
) -> crate::Result<Self> {
let auth_path = dirs.caches_meta_dir().await.join(AUTH_JSON);
let store = read_json(&auth_path, io_semaphore).await.ok();
if let Some(store) = store {
Ok(store)
} else {
Ok(Self {
users: HashMap::new(),
token: None,
default_user: None,
})
}
}
#[tracing::instrument(skip(self))]
pub async fn save(&self) -> crate::Result<()> {
let state = State::get().await?;
let auth_path =
state.directories.caches_meta_dir().await.join(AUTH_JSON);
write(&auth_path, &serde_json::to_vec(&self)?, &state.io_semaphore)
.await?;
Ok(())
}
#[tracing::instrument(skip(self))]
async fn refresh_and_get_device_token(
&mut self,
) -> crate::Result<(DeviceTokenKey, DeviceToken)> {
macro_rules! generate_key {
($self:ident, $generate_key:expr, $device_token:expr, $SaveDeviceToken:path) => {{
let key = generate_key()?;
let token = device_token(&key).await?;
self.token = Some(SaveDeviceToken {
id: key.id.clone(),
private_key: key
.key
.to_pkcs8_pem(LineEnding::default())
.map_err(|err| {
MinecraftAuthenticationError::PEMSerialize(err)
})?
.to_string(),
x: key.x.clone(),
y: key.y.clone(),
token: token.clone(),
});
self.save().await?;
(key, token)
}};
}
let (key, token) = if let Some(ref token) = self.token {
if token.token.not_after > Utc::now() {
if let Ok(private_key) =
SigningKey::from_pkcs8_pem(&token.private_key)
{
(
DeviceTokenKey {
id: token.id.clone(),
key: private_key,
x: token.x.clone(),
y: token.y.clone(),
},
token.token.clone(),
)
} else {
generate_key!(
self,
generate_key,
device_token,
SaveDeviceToken
)
}
} else {
generate_key!(self, generate_key, device_token, SaveDeviceToken)
}
} else {
generate_key!(self, generate_key, device_token, SaveDeviceToken)
};
Ok((key, token))
}
#[tracing::instrument(skip(self))]
pub async fn login_begin(&mut self) -> crate::Result<MinecraftLoginFlow> {
let (key, token) = self.refresh_and_get_device_token().await?;
let challenge = generate_oauth_challenge();
let (session_id, redirect_uri) =
sisu_authenticate(&token.token, &challenge, &key).await?;
Ok(MinecraftLoginFlow {
challenge,
session_id,
redirect_uri: redirect_uri.msa_oauth_redirect,
})
}
#[tracing::instrument(skip(self))]
pub async fn login_finish(
&mut self,
code: &str,
flow: MinecraftLoginFlow,
) -> crate::Result<Credentials> {
let (key, token) = self.refresh_and_get_device_token().await?;
let oauth_token = oauth_token(code, &flow.challenge).await?;
let sisu_authorize = sisu_authorize(
Some(&flow.session_id),
&oauth_token.access_token,
&token.token,
&key,
)
.await?;
let xbox_token =
xsts_authorize(sisu_authorize, &token.token, &key).await?;
let minecraft_token = minecraft_token(xbox_token).await?;
minecraft_entitlements(&minecraft_token.access_token).await?;
let profile = minecraft_profile(&minecraft_token.access_token).await?;
let profile_id = profile.id.unwrap_or_default();
let credentials = Credentials {
id: profile_id,
username: profile.name,
access_token: minecraft_token.access_token,
refresh_token: oauth_token.refresh_token,
expires: Utc::now()
+ Duration::seconds(oauth_token.expires_in as i64),
};
self.users.insert(profile_id, credentials.clone());
if self.default_user.is_none() {
self.default_user = Some(profile_id);
}
self.save().await?;
Ok(credentials)
}
#[tracing::instrument(skip(self))]
pub async fn get_default_credential(
&mut self,
) -> crate::Result<Option<Credentials>> {
let credentials = if let Some(default_user) = self.default_user {
if let Some(creds) = self.users.get(&default_user) {
Some(creds)
} else {
self.users.values().next()
}
} else {
self.users.values().next()
};
if let Some(creds) = credentials {
if self.default_user != Some(creds.id) {
self.default_user = Some(creds.id);
self.save().await?;
}
if creds.expires < Utc::now() {
let cred_id = creds.id;
let profile_name = creds.username.clone();
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
let (key, token) = self.refresh_and_get_device_token().await?;
let sisu_authorize = sisu_authorize(
None,
&oauth_token.access_token,
&token.token,
&key,
)
.await?;
let xbox_token =
xsts_authorize(sisu_authorize, &token.token, &key).await?;
let minecraft_token = minecraft_token(xbox_token).await?;
let val = Credentials {
id: cred_id,
username: profile_name,
access_token: minecraft_token.access_token,
refresh_token: oauth_token.refresh_token,
expires: Utc::now()
+ Duration::seconds(oauth_token.expires_in as i64),
};
self.users.insert(val.id, val.clone());
self.save().await?;
Ok(Some(val))
} else {
Ok(Some(creds.clone()))
}
} else {
Ok(None)
}
}
#[tracing::instrument(skip(self))]
pub async fn remove(
&mut self,
id: Uuid,
) -> crate::Result<Option<Credentials>> {
let val = self.users.remove(&id);
self.save().await?;
Ok(val)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Credentials {
pub id: Uuid,
pub username: String,
pub access_token: String,
pub refresh_token: String,
pub expires: DateTime<Utc>,
}
const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
// flow steps
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DeviceToken {
pub issue_instant: DateTime<Utc>,
pub not_after: DateTime<Utc>,
pub token: String,
pub display_claims: HashMap<String, serde_json::Value>,
}
#[tracing::instrument(skip(key))]
pub async fn device_token(
key: &DeviceTokenKey,
) -> Result<DeviceToken, MinecraftAuthenticationError> {
Ok(send_signed_request(
None,
"https://device.auth.xboxlive.com/device/authenticate",
"/device/authenticate",
json!({
"Properties": {
"AuthMethod": "ProofOfPossession",
"Id": format!("{{{}}}", key.id),
"DeviceType": "Win32",
"Version": "10.16.0",
"ProofKey": {
"kty": "EC",
"x": key.x,
"y": key.y,
"crv": "P-256",
"alg": "ES256",
"use": "sig"
}
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}),
key,
MinecraftAuthStep::GetDeviceToken,
)
.await?
.1)
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct RedirectUri {
pub msa_oauth_redirect: String,
}
#[tracing::instrument(skip(key))]
async fn sisu_authenticate(
token: &str,
challenge: &str,
key: &DeviceTokenKey,
) -> Result<(String, RedirectUri), MinecraftAuthenticationError> {
let (headers, res) = send_signed_request(
None,
"https://sisu.xboxlive.com/authenticate",
"/authenticate",
json!({
"AppId": MICROSOFT_CLIENT_ID,
"DeviceToken": token,
"Offers": [
REQUESTED_SCOPES
],
"Query": {
"code_challenge": challenge,
"code_challenge_method": "plain",
"state": "",
"prompt": "select_account"
},
"RedirectUri": REDIRECT_URL,
"Sandbox": "RETAIL",
"TokenType": "code",
}),
key,
MinecraftAuthStep::SisuAuthenicate,
)
.await?;
let session_id = headers
.get("X-SessionId")
.and_then(|x| x.to_str().ok())
.unwrap()
.to_string();
Ok((session_id, res))
}
#[derive(Deserialize)]
struct OAuthToken {
// pub token_type: String,
pub expires_in: u64,
// pub scope: String,
pub access_token: String,
pub refresh_token: String,
// pub user_id: String,
// pub foci: String,
}
#[tracing::instrument]
async fn oauth_token(
code: &str,
challenge: &str,
) -> Result<OAuthToken, MinecraftAuthenticationError> {
let mut query = HashMap::new();
query.insert("client_id", "00000000402b5328");
query.insert("code", code);
query.insert("code_verifier", challenge);
query.insert("grant_type", "authorization_code");
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
let res = auth_retry(|| {
REQWEST_CLIENT
.post("https://login.live.com/oauth20_token.srf")
.header("Accept", "application/json")
.form(&query)
.send()
})
.await
.map_err(|source| MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::GetOAuthToken,
})?;
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::GetOAuthToken,
}
})?;
serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::GetOAuthToken,
}
})
}
#[tracing::instrument]
async fn oauth_refresh(
refresh_token: &str,
) -> Result<OAuthToken, MinecraftAuthenticationError> {
let mut query = HashMap::new();
query.insert("client_id", "00000000402b5328");
query.insert("refresh_token", refresh_token);
query.insert("grant_type", "refresh_token");
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
let res = auth_retry(|| {
REQWEST_CLIENT
.post("https://login.live.com/oauth20_token.srf")
.header("Accept", "application/json")
.form(&query)
.send()
})
.await
.map_err(|source| MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::RefreshOAuthToken,
})?;
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::RefreshOAuthToken,
}
})?;
serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::RefreshOAuthToken,
}
})
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct SisuAuthorize {
// pub authorization_token: DeviceToken,
// pub device_token: String,
// pub sandbox: String,
pub title_token: DeviceToken,
pub user_token: DeviceToken,
// pub web_page: String,
}
#[tracing::instrument(skip(key))]
async fn sisu_authorize(
session_id: Option<&str>,
access_token: &str,
device_token: &str,
key: &DeviceTokenKey,
) -> Result<SisuAuthorize, MinecraftAuthenticationError> {
Ok(send_signed_request(
None,
"https://sisu.xboxlive.com/authorize",
"/authorize",
json!({
"AccessToken": format!("t={access_token}"),
"AppId": "00000000402b5328",
"DeviceToken": device_token,
"ProofKey": {
"kty": "EC",
"x": key.x,
"y": key.y,
"crv": "P-256",
"alg": "ES256",
"use": "sig"
},
"Sandbox": "RETAIL",
"SessionId": session_id,
"SiteName": "user.auth.xboxlive.com",
}),
key,
MinecraftAuthStep::SisuAuthorize,
)
.await?
.1)
}
#[tracing::instrument(skip(key))]
async fn xsts_authorize(
authorize: SisuAuthorize,
device_token: &str,
key: &DeviceTokenKey,
) -> Result<DeviceToken, MinecraftAuthenticationError> {
Ok(send_signed_request(
None,
"https://xsts.auth.xboxlive.com/xsts/authorize",
"/xsts/authorize",
json!({
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT",
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [authorize.user_token.token],
"DeviceToken": device_token,
"TitleToken": authorize.title_token.token,
},
}),
key,
MinecraftAuthStep::XstsAuthorize,
)
.await?
.1)
}
#[derive(Deserialize)]
struct MinecraftToken {
// pub username: String,
pub access_token: String,
// pub token_type: String,
// pub expires_in: u64,
}
#[tracing::instrument]
async fn minecraft_token(
token: DeviceToken,
) -> Result<MinecraftToken, MinecraftAuthenticationError> {
let uhs = token
.display_claims
.get("xui")
.and_then(|x| x.get(0))
.and_then(|x| x.get("uhs"))
.and_then(|x| x.as_str().map(String::from))
.ok_or_else(|| MinecraftAuthenticationError::NoUserHash)?;
let token = token.token;
let res = auth_retry(|| {
REQWEST_CLIENT
.post("https://api.minecraftservices.com/launcher/login")
.header("Accept", "application/json")
.json(&json!({
"platform": "PC_LAUNCHER",
"xtoken": format!("XBL3.0 x={uhs};{token}"),
}))
.send()
})
.await
.map_err(|source| MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::MinecraftToken,
})?;
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::MinecraftToken,
}
})?;
serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::MinecraftToken,
}
})
}
#[derive(Deserialize)]
struct MinecraftProfile {
pub id: Option<Uuid>,
pub name: String,
}
#[tracing::instrument]
async fn minecraft_profile(
token: &str,
) -> Result<MinecraftProfile, MinecraftAuthenticationError> {
let res = auth_retry(|| {
REQWEST_CLIENT
.get("https://api.minecraftservices.com/minecraft/profile")
.header("Accept", "application/json")
.bearer_auth(token)
.send()
})
.await
.map_err(|source| MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::MinecraftProfile,
})?;
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::MinecraftProfile,
}
})?;
serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::MinecraftProfile,
}
})
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct MinecraftEntitlements {}
#[tracing::instrument]
async fn minecraft_entitlements(
token: &str,
) -> Result<MinecraftEntitlements, MinecraftAuthenticationError> {
let res = auth_retry(|| {
REQWEST_CLIENT
.get(format!("https://api.minecraftservices.com/entitlements/license?requestId={}", Uuid::new_v4()))
.header("Accept", "application/json")
.bearer_auth(token)
.send()
})
.await.map_err(|source| MinecraftAuthenticationError::Request { source, step: MinecraftAuthStep::MinecraftEntitlements })?;
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
step: MinecraftAuthStep::MinecraftEntitlements,
}
})?;
serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::MinecraftEntitlements,
}
})
}
// auth utils
#[tracing::instrument(skip(reqwest_request))]
async fn auth_retry<F>(
reqwest_request: impl Fn() -> F,
) -> Result<reqwest::Response, reqwest::Error>
where
F: Future<Output = Result<Response, reqwest::Error>>,
{
const RETRY_COUNT: usize = 9; // Does command 9 times
const RETRY_WAIT: std::time::Duration =
std::time::Duration::from_millis(250);
let mut resp = reqwest_request().await?;
for i in 0..RETRY_COUNT {
if resp.status().is_success() {
break;
}
tracing::debug!(
"Request failed with status code {}, retrying...",
resp.status()
);
if i < RETRY_COUNT - 1 {
tokio::time::sleep(RETRY_WAIT).await;
}
resp = reqwest_request().await?;
}
Ok(resp)
}
pub struct DeviceTokenKey {
pub id: String,
pub key: SigningKey,
pub x: String,
pub y: String,
}
#[tracing::instrument]
fn generate_key() -> Result<DeviceTokenKey, MinecraftAuthenticationError> {
let id = Uuid::new_v4().to_string();
let signing_key = SigningKey::random(&mut OsRng);
let public_key = VerifyingKey::from(&signing_key);
let encoded_point = public_key.to_encoded_point(false);
Ok(DeviceTokenKey {
id,
key: signing_key,
x: BASE64_URL_SAFE_NO_PAD.encode(encoded_point.x().unwrap()),
y: BASE64_URL_SAFE_NO_PAD.encode(encoded_point.y().unwrap()),
})
}
#[tracing::instrument(skip(key))]
async fn send_signed_request<T: DeserializeOwned>(
authorization: Option<&str>,
url: &str,
url_path: &str,
raw_body: serde_json::Value,
key: &DeviceTokenKey,
step: MinecraftAuthStep,
) -> Result<(HeaderMap, T), MinecraftAuthenticationError> {
let auth = authorization.map_or(Vec::new(), |v| v.as_bytes().to_vec());
let body = serde_json::to_vec(&raw_body).map_err(|source| {
MinecraftAuthenticationError::SerializeBody { source, step }
})?;
let time: u128 =
{ ((Utc::now().timestamp() as u128) + 11644473600) * 10000000 };
use byteorder::WriteBytesExt;
let mut buffer = Vec::new();
buffer.write_u32::<BigEndian>(1).map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
})?;
buffer.write_u8(0).map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
})?;
buffer
.write_u64::<BigEndian>(time as u64)
.map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest {
source,
step,
}
})?;
buffer.write_u8(0).map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
})?;
buffer.extend_from_slice("POST".as_bytes());
buffer.write_u8(0).map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
})?;
buffer.extend_from_slice(url_path.as_bytes());
buffer.write_u8(0).map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
})?;
buffer.extend_from_slice(&auth);
buffer.write_u8(0).map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
})?;
buffer.extend_from_slice(&body);
buffer.write_u8(0).map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
})?;
let ecdsa_sig: Signature = key.key.sign(&buffer);
let mut sig_buffer = Vec::new();
sig_buffer.write_i32::<BigEndian>(1).map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
})?;
sig_buffer
.write_u64::<BigEndian>(time as u64)
.map_err(|source| {
MinecraftAuthenticationError::ConstructingSignedRequest {
source,
step,
}
})?;
sig_buffer.extend_from_slice(&ecdsa_sig.r().to_bytes());
sig_buffer.extend_from_slice(&ecdsa_sig.s().to_bytes());
let signature = BASE64_STANDARD.encode(&sig_buffer);
let res = auth_retry(|| {
let mut request = REQWEST_CLIENT
.post(url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("x-xbl-contract-version", "1")
.header("signature", &signature);
if let Some(auth) = authorization {
request = request.header("Authorization", auth);
}
request.body(body.clone()).send()
})
.await
.map_err(|source| MinecraftAuthenticationError::Request { source, step })?;
let headers = res.headers().clone();
let res = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request { source, step }
})?;
let body = serde_json::from_str(&res).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: res,
step,
}
})?;
Ok((headers, body))
}
#[tracing::instrument]
fn generate_oauth_challenge() -> String {
let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
}

View File

@ -5,7 +5,6 @@ use std::path::PathBuf;
use crate::event::LoadingBarType; use crate::event::LoadingBarType;
use crate::loading_join; use crate::loading_join;
use crate::state::users::Users;
use crate::util::fetch::{self, FetchSemaphore, IoSemaphore}; use crate::util::fetch::{self, FetchSemaphore, IoSemaphore};
use notify::RecommendedWatcher; use notify::RecommendedWatcher;
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer}; use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
@ -32,14 +31,9 @@ pub use self::settings::*;
mod projects; mod projects;
pub use self::projects::*; pub use self::projects::*;
mod users;
mod children; mod children;
pub use self::children::*; pub use self::children::*;
mod auth_task;
pub use self::auth_task::*;
mod tags; mod tags;
pub use self::tags::*; pub use self::tags::*;
@ -52,6 +46,9 @@ pub use self::safe_processes::*;
mod discord; mod discord;
pub use self::discord::*; pub use self::discord::*;
mod minecraft_auth;
pub use self::minecraft_auth::*;
mod mr_auth; mod mr_auth;
pub use self::mr_auth::*; pub use self::mr_auth::*;
@ -87,9 +84,7 @@ pub struct State {
/// Launcher processes that should be safely exited on shutdown /// Launcher processes that should be safely exited on shutdown
pub(crate) safety_processes: RwLock<SafeProcesses>, pub(crate) safety_processes: RwLock<SafeProcesses>,
/// Launcher user account info /// Launcher user account info
pub(crate) users: RwLock<Users>, pub(crate) users: RwLock<MinecraftAuthStore>,
/// Authentication flow
pub auth_flow: RwLock<AuthTask>,
/// Modrinth Credentials Store /// Modrinth Credentials Store
pub credentials: RwLock<CredentialsStore>, pub credentials: RwLock<CredentialsStore>,
/// Modrinth auth flow /// Modrinth auth flow
@ -172,7 +167,7 @@ impl State {
&fetch_semaphore, &fetch_semaphore,
&CredentialsStore(None), &CredentialsStore(None),
); );
let users_fut = Users::init(&directories, &io_semaphore); let users_fut = MinecraftAuthStore::init(&directories, &io_semaphore);
let creds_fut = CredentialsStore::init(&directories, &io_semaphore); let creds_fut = CredentialsStore::init(&directories, &io_semaphore);
// Launcher data // Launcher data
let (metadata, profiles, tags, users, creds) = loading_join! { let (metadata, profiles, tags, users, creds) = loading_join! {
@ -184,7 +179,6 @@ impl State {
creds_fut, creds_fut,
}?; }?;
let auth_flow = AuthTask::new();
let safety_processes = SafeProcesses::new(); let safety_processes = SafeProcesses::new();
let discord_rpc = DiscordGuard::init(is_offline).await?; let discord_rpc = DiscordGuard::init(is_offline).await?;
@ -217,7 +211,6 @@ impl State {
profiles: RwLock::new(profiles), profiles: RwLock::new(profiles),
users: RwLock::new(users), users: RwLock::new(users),
children: RwLock::new(children), children: RwLock::new(children),
auth_flow: RwLock::new(auth_flow),
credentials: RwLock::new(creds), credentials: RwLock::new(creds),
tags: RwLock::new(tags), tags: RwLock::new(tags),
discord_rpc, discord_rpc,
@ -253,9 +246,8 @@ impl State {
let res4 = Profiles::update_projects(); let res4 = Profiles::update_projects();
let res5 = Settings::update_java(); let res5 = Settings::update_java();
let res6 = CredentialsStore::update_creds(); let res6 = CredentialsStore::update_creds();
let res7 = Settings::update_default_user();
let _ = join!(res1, res2, res3, res4, res5, res6, res7); let _ = join!(res1, res2, res3, res4, res5, res6);
} }
} }
}); });

View File

@ -24,7 +24,6 @@ pub struct Settings {
pub custom_java_args: Vec<String>, pub custom_java_args: Vec<String>,
pub custom_env_args: Vec<(String, String)>, pub custom_env_args: Vec<(String, String)>,
pub java_globals: JavaGlobals, pub java_globals: JavaGlobals,
pub default_user: Option<uuid::Uuid>,
pub hooks: Hooks, pub hooks: Hooks,
pub max_concurrent_downloads: usize, pub max_concurrent_downloads: usize,
pub max_concurrent_writes: usize, pub max_concurrent_writes: usize,
@ -93,7 +92,6 @@ impl Settings {
custom_java_args: Vec::new(), custom_java_args: Vec::new(),
custom_env_args: Vec::new(), custom_env_args: Vec::new(),
java_globals: JavaGlobals::new(), java_globals: JavaGlobals::new(),
default_user: None,
hooks: Hooks::default(), hooks: Hooks::default(),
max_concurrent_downloads: 10, max_concurrent_downloads: 10,
max_concurrent_writes: 10, max_concurrent_writes: 10,
@ -152,32 +150,6 @@ impl Settings {
}; };
} }
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn update_default_user() {
let res = async {
let state = State::get().await?;
let settings_read = state.settings.read().await;
if settings_read.default_user.is_none() {
drop(settings_read);
let users = state.users.read().await;
let user = users.0.iter().next().map(|(id, _)| *id);
state.settings.write().await.default_user = user;
}
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
tracing::warn!("Unable to update default user: {err}")
}
};
}
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub async fn sync(&self, to: &Path) -> crate::Result<()> { pub async fn sync(&self, to: &Path) -> crate::Result<()> {
fs::write(to, serde_json::to_vec(self)?) fs::write(to, serde_json::to_vec(self)?)

View File

@ -1,70 +0,0 @@
//! User login info
use crate::auth::Credentials;
use crate::data::DirectoryInfo;
use crate::util::fetch::{read_json, write, IoSemaphore};
use crate::State;
use std::collections::HashMap;
use uuid::Uuid;
const USERS_JSON: &str = "users.json";
/// The set of users stored in the launcher
#[derive(Clone)]
pub(crate) struct Users(pub(crate) HashMap<Uuid, Credentials>);
impl Users {
pub async fn init(
dirs: &DirectoryInfo,
io_semaphore: &IoSemaphore,
) -> crate::Result<Self> {
let users_path = dirs.caches_meta_dir().await.join(USERS_JSON);
let users = read_json(&users_path, io_semaphore).await.ok();
if let Some(users) = users {
Ok(Self(users))
} else {
Ok(Self(HashMap::new()))
}
}
pub async fn save(&self) -> crate::Result<()> {
let state = State::get().await?;
let users_path =
state.directories.caches_meta_dir().await.join(USERS_JSON);
write(
&users_path,
&serde_json::to_vec(&self.0)?,
&state.io_semaphore,
)
.await?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn insert(
&mut self,
credentials: &Credentials,
) -> crate::Result<&Self> {
self.0.insert(credentials.id, credentials.clone());
self.save().await?;
Ok(self)
}
#[tracing::instrument(skip(self))]
pub fn contains(&self, id: Uuid) -> bool {
self.0.contains_key(&id)
}
#[tracing::instrument(skip(self))]
pub fn get(&self, id: Uuid) -> Option<Credentials> {
self.0.get(&id).cloned()
}
#[tracing::instrument(skip(self))]
pub async fn remove(&mut self, id: Uuid) -> crate::Result<&Self> {
self.0.remove(&id);
self.save().await?;
Ok(self)
}
}

View File

@ -1,34 +0,0 @@
[package]
name = "theseus_cli"
version = "0.6.3"
authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
theseus = { path = "../theseus", features = ["cli"] }
daedalus = {version = "0.1.15", features = ["bincode"]}
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3"
argh = "0.1"
paris = { version = "1.5", features = ["macros", "no_logger"] }
dialoguer = "0.11.0"
tabled = "0.15.0"
dirs = "5.0.1"
uuid = {version = "1.1", features = ["v4", "serde"]}
url = "2.2"
color-eyre = "0.6"
eyre = "0.6"
tracing = "0.1"
tracing-error = "0.2"
tracing-futures = "0.2"
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
dunce = "1.0.3"
webbrowser = "0.8.13"
[target.'cfg(windows)'.dependencies]
winreg = "0.52.0"

View File

@ -1,52 +0,0 @@
use eyre::Result;
use futures::TryFutureExt;
use paris::*;
use tracing_error::ErrorLayer;
use tracing_futures::WithSubscriber;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
#[macro_use]
mod util;
mod subcommands;
#[derive(argh::FromArgs, Debug)]
/// The official Modrinth CLI
pub struct Args {
#[argh(subcommand)]
pub subcommand: subcommands::Subcommand,
}
#[tracing::instrument]
fn main() -> Result<()> {
let args = argh::from_env::<Args>();
color_eyre::install()?;
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))?;
let format = fmt::layer()
.without_time()
.with_writer(std::io::stderr)
.with_target(false)
.compact();
tracing_subscriber::registry()
.with(format)
.with(filter)
.with(ErrorLayer::default())
.init();
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(
async move {
args.dispatch()
.inspect_err(|_| error!("An error has occurred!\n"))
.await
}
.with_current_subscriber(),
)
}

View File

@ -1,20 +0,0 @@
use eyre::Result;
mod profile;
mod user;
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand)]
pub enum Subcommand {
Profile(profile::ProfileCommand),
User(user::UserCommand),
}
impl crate::Args {
pub async fn dispatch(&self) -> Result<()> {
dispatch!(self.subcommand, (self) => {
Subcommand::Profile,
Subcommand::User
})
}
}

View File

@ -1,372 +0,0 @@
//! Profile management subcommand
use crate::util::table_path_display;
use crate::util::{confirm_async, prompt_async, select_async, table};
use daedalus::modded::LoaderVersion;
use dunce::canonicalize;
use eyre::{ensure, Result};
use futures::prelude::*;
use paris::*;
use std::path::{Path, PathBuf};
use tabled::settings::object::Columns;
use tabled::settings::{Modify, Width};
use tabled::Tabled;
use theseus::prelude::*;
use theseus::profile::create::profile_create;
use tokio::fs;
use tokio_stream::wrappers::ReadDirStream;
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand, name = "profile")]
/// manage Minecraft instances
pub struct ProfileCommand {
#[argh(subcommand)]
action: ProfileSubcommand,
}
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand)]
pub enum ProfileSubcommand {
Init(ProfileInit),
List(ProfileList),
Remove(ProfileRemove),
Run(ProfileRun),
}
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand, name = "init")]
/// create a new profile and manage it with Theseus
pub struct ProfileInit {
#[argh(positional, default = "std::env::current_dir().unwrap()")]
/// the path of the newly created profile
path: PathBuf,
#[argh(option)]
/// the name of the profile
name: Option<String>,
#[argh(option)]
/// the game version of the profile
game_version: Option<String>,
#[argh(option, from_str_fn(modloader_from_str))]
/// the modloader to use
modloader: Option<ModLoader>,
#[argh(option)]
/// the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
loader_version: Option<String>,
}
impl ProfileInit {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &ProfileCommand,
) -> Result<()> {
// TODO: validate inputs from args early
let state = State::get().await?;
let metadata = state.metadata.read().await;
if self.path.exists() {
ensure!(
self.path.is_dir(),
"Attempted to create profile in something other than a folder!"
);
ensure!(
!self.path.join("profile.json").exists(),
"Profile already exists! Perhaps you want `profile add` instead?"
);
if ReadDirStream::new(fs::read_dir(&self.path).await?)
.next()
.await
.is_some()
{
warn!("You are trying to create a profile in a non-empty directory. If this is an instance from another launcher, please be sure to properly fill the profile.json fields!");
if !confirm_async(
String::from("Do you wish to continue"),
false,
)
.await?
{
eyre::bail!("Aborted!");
}
}
} else {
fs::create_dir_all(&self.path).await?;
}
info!(
"Creating profile at path {}",
&canonicalize(&self.path)?.display()
);
// TODO: abstract default prompting
let name = match &self.name {
Some(name) => name.clone(),
None => {
let default = self.path.file_name().unwrap().to_string_lossy();
prompt_async(
String::from("Instance name"),
Some(default.into_owned()),
)
.await?
}
};
let game_version = match &self.game_version {
Some(version) => version.clone(),
None => {
let default = &metadata.minecraft.latest.release;
prompt_async(
String::from("Game version"),
Some(default.clone()),
)
.await?
}
};
let loader = match &self.modloader {
Some(loader) => *loader,
None => {
let choice = select_async(
"Modloader".to_owned(),
&["vanilla", "fabric", "forge"],
)
.await?;
match choice {
0 => ModLoader::Vanilla,
1 => ModLoader::Fabric,
2 => ModLoader::Forge,
_ => eyre::bail!(
"Invalid modloader ID: {choice}. This is a bug in the launcher!"
),
}
}
};
let loader = if loader != ModLoader::Vanilla {
let version = match &self.loader_version {
Some(version) => String::from(version),
None => prompt_async(
String::from(
"Modloader version (latest, stable, or a version ID)",
),
Some(String::from("latest")),
)
.await?,
};
let filter = |it: &LoaderVersion| match version.as_str() {
"latest" => true,
"stable" => it.stable,
id => it.id == *id,
};
let loader_data = match loader {
ModLoader::Forge => &metadata.forge,
ModLoader::Fabric => &metadata.fabric,
_ => eyre::bail!("Could not get manifest for loader {loader}. This is a bug in the CLI!"),
};
let loaders = &loader_data.game_versions
.iter()
.find(|it| it.id == game_version)
.ok_or_else(|| eyre::eyre!("Modloader {loader} unsupported for Minecraft version {game_version}"))?
.loaders;
let loader_version = loaders
.iter()
.find(|&x| filter(x))
.cloned()
.ok_or_else(|| {
eyre::eyre!("Invalid version {version} for modloader {loader}")
})?;
Some((loader_version, loader))
} else {
None
};
profile_create(
name,
game_version,
loader.clone().map(|x| x.1).unwrap_or(ModLoader::Vanilla),
loader.map(|x| x.0.id),
None,
None,
None,
None,
None,
)
.await?;
success!(
"Successfully created instance, it is now available to use with Theseus!"
);
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// list all managed profiles
#[argh(subcommand, name = "list")]
pub struct ProfileList {}
#[derive(Tabled)]
struct ProfileRow<'a> {
name: &'a str,
#[tabled(display_with = "table_path_display")]
path: &'a Path,
#[tabled(rename = "game version")]
game_version: &'a str,
loader: &'a ModLoader,
#[tabled(rename = "loader version")]
loader_version: &'a str,
}
impl<'a> From<&'a Profile> for ProfileRow<'a> {
fn from(it: &'a Profile) -> Self {
Self {
name: &it.metadata.name,
path: Path::new(&it.metadata.name),
game_version: &it.metadata.game_version,
loader: &it.metadata.loader,
loader_version: it
.metadata
.loader_version
.as_ref()
.map_or("", |it| &it.id),
}
}
}
impl<'a> From<&'a Path> for ProfileRow<'a> {
fn from(it: &'a Path) -> Self {
Self {
name: "?",
path: it,
game_version: "?",
loader: &ModLoader::Vanilla,
loader_version: "?",
}
}
}
impl ProfileList {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &ProfileCommand,
) -> Result<()> {
let profiles = profile::list(None).await?;
let rows = profiles.values().map(ProfileRow::from);
let mut table = table(rows);
table.with(Modify::new(Columns::new(1..=1)).with(Width::wrap(40)));
println!("{table}");
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// unmanage a profile
#[argh(subcommand, name = "remove")]
pub struct ProfileRemove {
#[argh(positional, default = "std::env::current_dir().unwrap()")]
/// the profile to get rid of
profile: PathBuf,
}
impl ProfileRemove {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &ProfileCommand,
) -> Result<()> {
let profile =
ProfilePathId::from_fs_path(canonicalize(&self.profile)?).await?;
info!("Removing profile {} from Theseus", self.profile.display());
if confirm_async(String::from("Do you wish to continue"), true).await? {
profile::remove(&profile).await?;
State::sync().await?;
success!("Profile removed!");
} else {
error!("Aborted!");
}
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// run a profile
#[argh(subcommand, name = "run")]
pub struct ProfileRun {
#[argh(positional, default = "std::env::current_dir().unwrap()")]
/// the profile to run
profile: PathBuf,
#[argh(option)]
/// the user to authenticate with
user: Option<uuid::Uuid>,
}
impl ProfileRun {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &ProfileCommand,
) -> Result<()> {
info!("Starting profile at path {}...", self.profile.display());
let path = canonicalize(&self.profile)?;
let id = future::ready(self.user.ok_or(()))
.or_else(|_| async move {
let state = State::get().await?;
let settings = state.settings.read().await;
settings.default_user
.ok_or(eyre::eyre!(
"Could not find any users, please add one using the `user add` command."
))
})
.await?;
let credentials = auth::refresh(id).await?;
let profile_path_id = ProfilePathId::from_fs_path(path).await?;
let proc_lock =
profile::run_credentials(&profile_path_id, &credentials).await?;
let mut proc = proc_lock.write().await;
process::wait_for(&mut proc).await?;
success!("Process exited successfully!");
Ok(())
}
}
impl ProfileCommand {
pub async fn run(&self, args: &crate::Args) -> Result<()> {
dispatch!(&self.action, (args, self) => {
ProfileSubcommand::Init,
ProfileSubcommand::List,
ProfileSubcommand::Remove,
ProfileSubcommand::Run
})
}
}
fn modloader_from_str(it: &str) -> core::result::Result<ModLoader, String> {
match it {
"vanilla" => Ok(ModLoader::Vanilla),
"forge" => Ok(ModLoader::Forge),
"fabric" => Ok(ModLoader::Fabric),
"quilt" => Ok(ModLoader::Quilt),
"neoforge" => Ok(ModLoader::NeoForge),
_ => Err(String::from("Invalid modloader: {it}")),
}
}

View File

@ -1,181 +0,0 @@
//! User management subcommand
use crate::util::{confirm_async, table};
use eyre::Result;
use paris::*;
use tabled::Tabled;
use theseus::prelude::*;
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand, name = "user")]
/// manage Minecraft accounts
pub struct UserCommand {
#[argh(subcommand)]
action: UserSubcommand,
}
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand)]
pub enum UserSubcommand {
Add(UserAdd),
List(UserList),
Remove(UserRemove),
SetDefault(UserDefault),
}
#[derive(argh::FromArgs, Debug)]
/// add a new user to Theseus
#[argh(subcommand, name = "add")]
pub struct UserAdd {
#[argh(option)]
/// the browser to authenticate using
browser: Option<webbrowser::Browser>,
}
impl UserAdd {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &UserCommand,
) -> Result<()> {
info!("Adding new user account to Theseus");
info!("A browser window will now open, follow the login flow there.");
let login = auth::authenticate_begin_flow().await?;
let flow = tokio::spawn(auth::authenticate_await_complete_flow());
info!("Opening browser window at {}", login.verification_uri);
info!("Your code is {}", login.user_code);
match self.browser {
Some(browser) => webbrowser::open_browser(
browser,
login.verification_uri.as_str(),
),
None => webbrowser::open(login.verification_uri.as_str()),
}?;
let credentials = flow.await??;
State::sync().await?;
success!("Logged in user {}.", credentials.username);
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// list all known users
#[argh(subcommand, name = "list")]
pub struct UserList {}
#[derive(Tabled)]
struct UserRow<'a> {
username: &'a str,
id: uuid::Uuid,
default: bool,
}
impl<'a> UserRow<'a> {
pub fn from(
credentials: &'a Credentials,
default: Option<uuid::Uuid>,
) -> Self {
Self {
username: &credentials.username,
id: credentials.id,
default: Some(credentials.id) == default,
}
}
}
impl UserList {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &UserCommand,
) -> Result<()> {
let state = State::get().await?;
let default = state.settings.read().await.default_user;
let users = auth::users().await?;
let rows = users.iter().map(|user| UserRow::from(user, default));
let table = table(rows);
println!("{table}");
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// remove a user
#[argh(subcommand, name = "remove")]
pub struct UserRemove {
/// the user to remove
#[argh(positional)]
user: uuid::Uuid,
}
impl UserRemove {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &UserCommand,
) -> Result<()> {
info!("Removing user {}", self.user.as_hyphenated());
if confirm_async(String::from("Do you wish to continue"), true).await? {
if !auth::has_user(self.user).await? {
warn!("Profile was not managed by Theseus!");
} else {
auth::remove_user(self.user).await?;
State::sync().await?;
success!("User removed!");
}
} else {
error!("Aborted!");
}
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// set the default user
#[argh(subcommand, name = "set-default")]
pub struct UserDefault {
/// the user to set as default
#[argh(positional)]
user: uuid::Uuid,
}
impl UserDefault {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &UserCommand,
) -> Result<()> {
info!("Setting user {} as default", self.user.as_hyphenated());
let state = State::get().await?;
let mut settings = state.settings.write().await;
if settings.default_user == Some(self.user) {
warn!("User is already the default!");
} else {
settings.default_user = Some(self.user);
success!("User set as default!");
}
Ok(())
}
}
impl UserCommand {
pub async fn run(&self, args: &crate::Args) -> Result<()> {
dispatch!(&self.action, (args, self) => {
UserSubcommand::Add,
UserSubcommand::List,
UserSubcommand::Remove,
UserSubcommand::SetDefault
})
}
}

View File

@ -1,95 +0,0 @@
use dialoguer::{Confirm, Input, Select};
use eyre::Result;
use std::{borrow::Cow, path::Path};
use tabled::settings::Style;
use tabled::{Table, Tabled};
// TODO: make primarily async to avoid copies
// Prompting helpers
pub fn prompt(prompt: &str, default: Option<String>) -> Result<String> {
let prompt = match default.as_deref() {
Some("") => Cow::Owned(format!("{prompt} (optional)")),
Some(default) => Cow::Owned(format!("{prompt} (default: {default})")),
None => Cow::Borrowed(prompt),
};
print_prompt(&prompt);
let mut input = Input::<String>::new().with_prompt("").show_default(false);
if let Some(default) = default {
input = input.default(default);
}
Ok(input.interact_text()?.trim().to_owned())
}
pub async fn prompt_async(
text: String,
default: Option<String>,
) -> Result<String> {
tokio::task::spawn_blocking(move || prompt(&text, default)).await?
}
// Selection helpers
pub fn select(prompt: &str, choices: &[&str]) -> Result<usize> {
print_prompt(prompt);
let res = Select::new().items(choices).default(0).interact()?;
eprintln!("> {}", choices[res]);
Ok(res)
}
pub async fn select_async(
prompt: String,
choices: &'static [&'static str],
) -> Result<usize> {
tokio::task::spawn_blocking(move || select(&prompt, choices)).await?
}
// Confirmation helpers
pub fn confirm(prompt: &str, default: bool) -> Result<bool> {
print_prompt(prompt);
Ok(Confirm::new().default(default).interact()?)
}
pub async fn confirm_async(prompt: String, default: bool) -> Result<bool> {
tokio::task::spawn_blocking(move || confirm(&prompt, default)).await?
}
// Table helpers
pub fn table<T: Tabled>(rows: impl IntoIterator<Item = T>) -> Table {
Table::new(rows).with(Style::psql()).clone()
}
pub fn table_path_display(path: &Path) -> String {
let mut res = path.display().to_string();
if let Some(home_dir) = dirs::home_dir() {
res = res.replace(&home_dir.display().to_string(), "~");
}
res
}
// Dispatch macros
macro_rules! dispatch {
($on:expr, $args:tt => {$($option:path),+}) => {
match $on {
$($option (ref cmd) => dispatch!(@apply cmd => $args)),+
}
};
(@apply $cmd:expr => ($($args:expr),*)) => {{
use tracing_futures::WithSubscriber;
$cmd.run($($args),*).with_current_subscriber().await
}};
}
// Internal helpers
fn print_prompt(prompt: &str) {
println!(
"{}",
paris::formatter::colorize_string(format!("<yellow>?</> {prompt}:"))
);
}

View File

@ -20,7 +20,6 @@
"ofetch": "^1.3.4", "ofetch": "^1.3.4",
"omorphia": "^0.4.41", "omorphia": "^0.4.41",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qrcode.vue": "^3.4.1",
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1", "tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",
"vue": "^3.4.21", "vue": "^3.4.21",

View File

@ -26,9 +26,6 @@ dependencies:
pinia: pinia:
specifier: ^2.1.7 specifier: ^2.1.7
version: 2.1.7(vue@3.4.21) version: 2.1.7(vue@3.4.21)
qrcode.vue:
specifier: ^3.4.1
version: 3.4.1(vue@3.4.21)
tauri-plugin-window-state-api: tauri-plugin-window-state-api:
specifier: github:tauri-apps/tauri-plugin-window-state#v1 specifier: github:tauri-apps/tauri-plugin-window-state#v1
version: github.com/tauri-apps/tauri-plugin-window-state/002cf15f6a1e4969a678a4ade680cd60477a8a53 version: github.com/tauri-apps/tauri-plugin-window-state/002cf15f6a1e4969a678a4ade680cd60477a8a53
@ -87,12 +84,12 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/@babel/helper-string-parser@7.24.1: /@babel/helper-string-parser@7.21.5:
resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
/@babel/helper-validator-identifier@7.22.20: /@babel/helper-validator-identifier@7.19.1:
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
/@babel/parser@7.24.4: /@babel/parser@7.24.4:
@ -100,14 +97,14 @@ packages:
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
'@babel/types': 7.24.0 '@babel/types': 7.22.4
/@babel/types@7.24.0: /@babel/types@7.22.4:
resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} resolution: {integrity: sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/helper-string-parser': 7.24.1 '@babel/helper-string-parser': 7.21.5
'@babel/helper-validator-identifier': 7.22.20 '@babel/helper-validator-identifier': 7.19.1
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
/@esbuild/aix-ppc64@0.20.2: /@esbuild/aix-ppc64@0.20.2:
@ -340,7 +337,7 @@ packages:
debug: 4.3.4 debug: 4.3.4
espree: 9.6.1 espree: 9.6.1
globals: 13.24.0 globals: 13.24.0
ignore: 5.3.1 ignore: 5.2.4
import-fresh: 3.3.0 import-fresh: 3.3.0
js-yaml: 4.1.0 js-yaml: 4.1.0
minimatch: 3.1.2 minimatch: 3.1.2
@ -354,12 +351,22 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
/@floating-ui/core@0.3.1:
resolution: {integrity: sha512-ensKY7Ub59u16qsVIFEo2hwTCqZ/r9oZZFh51ivcLGHfUwTn8l1Xzng8RJUe91H/UP8PeqeBronAGx0qmzwk2g==}
dev: false
/@floating-ui/core@1.6.0: /@floating-ui/core@1.6.0:
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
dependencies: dependencies:
'@floating-ui/utils': 0.2.1 '@floating-ui/utils': 0.2.1
dev: false dev: false
/@floating-ui/dom@0.1.10:
resolution: {integrity: sha512-4kAVoogvQm2N0XE0G6APQJuCNuErjOfPW8Ux7DFxh8+AfugWflwVJ5LDlHOwrwut7z/30NUvdtHzQ3zSip4EzQ==}
dependencies:
'@floating-ui/core': 0.3.1
dev: false
/@floating-ui/dom@1.1.1: /@floating-ui/dom@1.1.1:
resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==} resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==}
dependencies: dependencies:
@ -415,7 +422,7 @@ packages:
engines: {node: '>= 8'} engines: {node: '>= 8'}
dependencies: dependencies:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1 fastq: 1.15.0
dev: true dev: true
/@rollup/plugin-alias@5.1.0: /@rollup/plugin-alias@5.1.0:
@ -438,120 +445,120 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true dev: true
/@rollup/rollup-android-arm-eabi@4.14.0: /@rollup/rollup-android-arm-eabi@4.14.1:
resolution: {integrity: sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==} resolution: {integrity: sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-android-arm64@4.14.0: /@rollup/rollup-android-arm64@4.14.1:
resolution: {integrity: sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==} resolution: {integrity: sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-darwin-arm64@4.14.0: /@rollup/rollup-darwin-arm64@4.14.1:
resolution: {integrity: sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==} resolution: {integrity: sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-darwin-x64@4.14.0: /@rollup/rollup-darwin-x64@4.14.1:
resolution: {integrity: sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==} resolution: {integrity: sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.14.0: /@rollup/rollup-linux-arm-gnueabihf@4.14.1:
resolution: {integrity: sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==} resolution: {integrity: sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm64-gnu@4.14.0: /@rollup/rollup-linux-arm64-gnu@4.14.1:
resolution: {integrity: sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==} resolution: {integrity: sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm64-musl@4.14.0: /@rollup/rollup-linux-arm64-musl@4.14.1:
resolution: {integrity: sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==} resolution: {integrity: sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-powerpc64le-gnu@4.14.0: /@rollup/rollup-linux-powerpc64le-gnu@4.14.1:
resolution: {integrity: sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==} resolution: {integrity: sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==}
cpu: [ppc64le] cpu: [ppc64le]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-riscv64-gnu@4.14.0: /@rollup/rollup-linux-riscv64-gnu@4.14.1:
resolution: {integrity: sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==} resolution: {integrity: sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-s390x-gnu@4.14.0: /@rollup/rollup-linux-s390x-gnu@4.14.1:
resolution: {integrity: sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==} resolution: {integrity: sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-x64-gnu@4.14.0: /@rollup/rollup-linux-x64-gnu@4.14.1:
resolution: {integrity: sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==} resolution: {integrity: sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-x64-musl@4.14.0: /@rollup/rollup-linux-x64-musl@4.14.1:
resolution: {integrity: sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==} resolution: {integrity: sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-arm64-msvc@4.14.0: /@rollup/rollup-win32-arm64-msvc@4.14.1:
resolution: {integrity: sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==} resolution: {integrity: sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-ia32-msvc@4.14.0: /@rollup/rollup-win32-ia32-msvc@4.14.1:
resolution: {integrity: sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==} resolution: {integrity: sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-x64-msvc@4.14.0: /@rollup/rollup-win32-x64-msvc@4.14.1:
resolution: {integrity: sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==} resolution: {integrity: sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
@ -675,19 +682,23 @@ packages:
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
dev: false dev: false
/@types/eslint@8.56.7: /@types/eslint@8.40.0:
resolution: {integrity: sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==} resolution: {integrity: sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g==}
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.1
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.12
dev: true
/@types/estree@1.0.1:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
dev: true dev: true
/@types/estree@1.0.5: /@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: true dev: true
/@types/json-schema@7.0.15: /@types/json-schema@7.0.12:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
dev: true dev: true
/@ungap/structured-clone@1.2.0: /@ungap/structured-clone@1.2.0:
@ -739,6 +750,10 @@ packages:
'@vue/compiler-dom': 3.4.21 '@vue/compiler-dom': 3.4.21
'@vue/shared': 3.4.21 '@vue/shared': 3.4.21
/@vue/devtools-api@6.5.0:
resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
dev: false
/@vue/devtools-api@6.6.1: /@vue/devtools-api@6.6.1:
resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==} resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==}
dev: false dev: false
@ -823,8 +838,8 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true dev: true
/binary-extensions@2.3.0: /binary-extensions@2.2.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
@ -865,8 +880,8 @@ packages:
'@kurkle/color': 0.3.2 '@kurkle/color': 0.3.2
dev: false dev: false
/chokidar@3.6.0: /chokidar@3.5.3:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
dependencies: dependencies:
anymatch: 3.1.3 anymatch: 3.1.3
@ -1136,9 +1151,9 @@ packages:
file-entry-cache: 6.0.1 file-entry-cache: 6.0.1
find-up: 5.0.0 find-up: 5.0.0
glob-parent: 6.0.2 glob-parent: 6.0.2
globals: 13.24.0 globals: 13.20.0
graphemer: 1.4.0 graphemer: 1.4.0
ignore: 5.3.1 ignore: 5.2.4
imurmurhash: 0.1.4 imurmurhash: 0.1.4
is-glob: 4.0.3 is-glob: 4.0.3
is-path-inside: 3.0.3 is-path-inside: 3.0.3
@ -1203,8 +1218,8 @@ packages:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
dev: true dev: true
/fastq@1.17.1: /fastq@1.15.0:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
dependencies: dependencies:
reusify: 1.0.4 reusify: 1.0.4
dev: true dev: true
@ -1213,7 +1228,7 @@ packages:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0} engines: {node: ^10.12.0 || >=12.0.0}
dependencies: dependencies:
flat-cache: 3.2.0 flat-cache: 3.0.4
dev: true dev: true
/fill-range@7.0.1: /fill-range@7.0.1:
@ -1231,29 +1246,24 @@ packages:
path-exists: 4.0.0 path-exists: 4.0.0
dev: true dev: true
/flat-cache@3.2.0: /flat-cache@3.0.4:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
engines: {node: ^10.12.0 || >=12.0.0} engines: {node: ^10.12.0 || >=12.0.0}
dependencies: dependencies:
flatted: 3.3.1 flatted: 3.2.7
keyv: 4.5.4
rimraf: 3.0.2 rimraf: 3.0.2
dev: true dev: true
/flatted@3.3.1: /flatted@3.2.7:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true dev: true
/floating-vue@2.0.0(vue@3.4.21): /floating-vue@2.0.0-beta.20(vue@3.4.21):
resolution: {integrity: sha512-YSffLYOjoaaPPBZc7VQR2qMCQ7xeXuh7i8a2u8WOdSmkjTtKtZpj2aaJnLtZRHmehrMHyCgtSxLu8jFNNX2sVw==} resolution: {integrity: sha512-N68otcpp6WwcYC7zP8GeJqNZVdfvS7tEY88lwmuAHeqRgnfWx1Un8enzLxROyVnBDZ3TwUoUdj5IFg+bUT7JeA==}
peerDependencies: peerDependencies:
'@nuxt/kit': ^3.2.0
vue: ^3.2.0 vue: ^3.2.0
peerDependenciesMeta:
'@nuxt/kit':
optional: true
dependencies: dependencies:
'@floating-ui/dom': 1.1.1 '@floating-ui/dom': 0.1.10
vue: 3.4.21 vue: 3.4.21
vue-resize: 2.0.0-alpha.1(vue@3.4.21) vue-resize: 2.0.0-alpha.1(vue@3.4.21)
dev: false dev: false
@ -1309,6 +1319,13 @@ packages:
path-is-absolute: 1.0.1 path-is-absolute: 1.0.1
dev: true dev: true
/globals@13.20.0:
resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==}
engines: {node: '>=8'}
dependencies:
type-fest: 0.20.2
dev: true
/globals@13.24.0: /globals@13.24.0:
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1325,18 +1342,18 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/highlight.js@11.9.0: /highlight.js@11.8.0:
resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
dev: false dev: false
/ignore@5.3.1: /ignore@5.2.4:
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
dev: true dev: true
/immutable@4.3.5: /immutable@4.3.0:
resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==}
dev: true dev: true
/import-fresh@3.3.0: /import-fresh@3.3.0:
@ -1367,7 +1384,7 @@ packages:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
dependencies: dependencies:
binary-extensions: 2.3.0 binary-extensions: 2.2.0
dev: true dev: true
/is-extglob@2.1.1: /is-extglob@2.1.1:
@ -1403,10 +1420,6 @@ packages:
argparse: 2.0.1 argparse: 2.0.1
dev: true dev: true
/json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
dev: true
/json-schema-traverse@0.4.1: /json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true dev: true
@ -1415,12 +1428,6 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true dev: true
/keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
dependencies:
json-buffer: 3.0.1
dev: true
/levn@0.4.1: /levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -1463,8 +1470,8 @@ packages:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
/markdown-it@13.0.2: /markdown-it@13.0.1:
resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==}
hasBin: true hasBin: true
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
@ -1540,17 +1547,16 @@ packages:
dependencies: dependencies:
chart.js: 4.4.2 chart.js: 4.4.2
dayjs: 1.11.10 dayjs: 1.11.10
floating-vue: 2.0.0(vue@3.4.21) floating-vue: 2.0.0-beta.20(vue@3.4.21)
highlight.js: 11.9.0 highlight.js: 11.8.0
markdown-it: 13.0.2 markdown-it: 13.0.1
qrcode.vue: 3.4.1(vue@3.4.21) qrcode.vue: 3.4.0(vue@3.4.21)
vue: 3.4.21 vue: 3.4.21
vue-chartjs: 5.3.0(chart.js@4.4.2)(vue@3.4.21) vue-chartjs: 5.3.1(chart.js@4.4.2)(vue@3.4.21)
vue-router: 4.3.0(vue@3.4.21) vue-router: 4.3.0(vue@3.4.21)
vue-select: 4.0.0-beta.6(vue@3.4.21) vue-select: 4.0.0-beta.6(vue@3.4.21)
xss: 1.0.15 xss: 1.0.14
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit'
- typescript - typescript
dev: false dev: false
@ -1628,9 +1634,9 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@vue/devtools-api': 6.6.1 '@vue/devtools-api': 6.5.0
vue: 3.4.21 vue: 3.4.21
vue-demi: 0.14.7(vue@3.4.21) vue-demi: 0.14.5(vue@3.4.21)
dev: false dev: false
/postcss-selector-parser@6.0.16: /postcss-selector-parser@6.0.16:
@ -1660,13 +1666,13 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/punycode@2.3.1: /punycode@2.3.0:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/qrcode.vue@3.4.1(vue@3.4.21): /qrcode.vue@3.4.0(vue@3.4.21):
resolution: {integrity: sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==} resolution: {integrity: sha512-4XeImbv10Fin16Fl2DArCMhGyAdvIg2jb7vDT+hZiIAMg/6H6mz9nUZr/dR8jBcun5VzNzkiwKhiqOGbloinwA==}
peerDependencies: peerDependencies:
vue: ^3.0.0 vue: ^3.0.0
dependencies: dependencies:
@ -1709,28 +1715,28 @@ packages:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
/rollup@4.14.0: /rollup@4.14.1:
resolution: {integrity: sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==} resolution: {integrity: sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
optionalDependencies: optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.14.0 '@rollup/rollup-android-arm-eabi': 4.14.1
'@rollup/rollup-android-arm64': 4.14.0 '@rollup/rollup-android-arm64': 4.14.1
'@rollup/rollup-darwin-arm64': 4.14.0 '@rollup/rollup-darwin-arm64': 4.14.1
'@rollup/rollup-darwin-x64': 4.14.0 '@rollup/rollup-darwin-x64': 4.14.1
'@rollup/rollup-linux-arm-gnueabihf': 4.14.0 '@rollup/rollup-linux-arm-gnueabihf': 4.14.1
'@rollup/rollup-linux-arm64-gnu': 4.14.0 '@rollup/rollup-linux-arm64-gnu': 4.14.1
'@rollup/rollup-linux-arm64-musl': 4.14.0 '@rollup/rollup-linux-arm64-musl': 4.14.1
'@rollup/rollup-linux-powerpc64le-gnu': 4.14.0 '@rollup/rollup-linux-powerpc64le-gnu': 4.14.1
'@rollup/rollup-linux-riscv64-gnu': 4.14.0 '@rollup/rollup-linux-riscv64-gnu': 4.14.1
'@rollup/rollup-linux-s390x-gnu': 4.14.0 '@rollup/rollup-linux-s390x-gnu': 4.14.1
'@rollup/rollup-linux-x64-gnu': 4.14.0 '@rollup/rollup-linux-x64-gnu': 4.14.1
'@rollup/rollup-linux-x64-musl': 4.14.0 '@rollup/rollup-linux-x64-musl': 4.14.1
'@rollup/rollup-win32-arm64-msvc': 4.14.0 '@rollup/rollup-win32-arm64-msvc': 4.14.1
'@rollup/rollup-win32-ia32-msvc': 4.14.0 '@rollup/rollup-win32-ia32-msvc': 4.14.1
'@rollup/rollup-win32-x64-msvc': 4.14.0 '@rollup/rollup-win32-x64-msvc': 4.14.1
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
@ -1745,9 +1751,9 @@ packages:
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
chokidar: 3.6.0 chokidar: 3.5.3
immutable: 4.3.5 immutable: 4.3.0
source-map-js: 1.2.0 source-map-js: 1.0.2
dev: true dev: true
/semver@7.6.0: /semver@7.6.0:
@ -1775,6 +1781,11 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
dev: true
/source-map-js@1.2.0: /source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1798,8 +1809,8 @@ packages:
has-flag: 4.0.0 has-flag: 4.0.0
dev: true dev: true
/svgo@3.2.0: /svgo@3.0.2:
resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==} resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
@ -1807,7 +1818,6 @@ packages:
commander: 7.2.0 commander: 7.2.0
css-select: 5.1.0 css-select: 5.1.0
css-tree: 2.3.1 css-tree: 2.3.1
css-what: 6.1.0
csso: 5.0.5 csso: 5.0.5
picocolors: 1.0.0 picocolors: 1.0.0
dev: false dev: false
@ -1850,7 +1860,7 @@ packages:
/uri-js@4.4.1: /uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.0
dev: true dev: true
/util-deprecate@1.0.2: /util-deprecate@1.0.2:
@ -1864,7 +1874,7 @@ packages:
vite: '>=2' vite: '>=2'
dependencies: dependencies:
'@rollup/pluginutils': 4.2.1 '@rollup/pluginutils': 4.2.1
'@types/eslint': 8.56.7 '@types/eslint': 8.40.0
eslint: 8.57.0 eslint: 8.57.0
rollup: 2.79.1 rollup: 2.79.1
vite: 5.2.8(sass@1.74.1) vite: 5.2.8(sass@1.74.1)
@ -1875,7 +1885,7 @@ packages:
peerDependencies: peerDependencies:
vue: '>=3.2.13' vue: '>=3.2.13'
dependencies: dependencies:
svgo: 3.2.0 svgo: 3.0.2
vue: 3.4.21 vue: 3.4.21
dev: false dev: false
@ -1909,14 +1919,14 @@ packages:
dependencies: dependencies:
esbuild: 0.20.2 esbuild: 0.20.2
postcss: 8.4.38 postcss: 8.4.38
rollup: 4.14.0 rollup: 4.14.1
sass: 1.74.1 sass: 1.74.1
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
/vue-chartjs@5.3.0(chart.js@4.4.2)(vue@3.4.21): /vue-chartjs@5.3.1(chart.js@4.4.2)(vue@3.4.21):
resolution: {integrity: sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==} resolution: {integrity: sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==}
peerDependencies: peerDependencies:
chart.js: ^4.1.1 chart.js: ^4.1.1
vue: ^3.0.0-0 || ^2.7.0 vue: ^3.0.0-0 || ^2.7.0
@ -1925,8 +1935,8 @@ packages:
vue: 3.4.21 vue: 3.4.21
dev: false dev: false
/vue-demi@0.14.7(vue@3.4.21): /vue-demi@0.14.5(vue@3.4.21):
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
engines: {node: '>=12'} engines: {node: '>=12'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
@ -2038,8 +2048,8 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/xss@1.0.15: /xss@1.0.14:
resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} resolution: {integrity: sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
hasBin: true hasBin: true
dependencies: dependencies:

View File

@ -1,16 +1,15 @@
use crate::api::Result; use crate::api::Result;
use chrono::{Duration, Utc};
use tauri::plugin::TauriPlugin; use tauri::plugin::TauriPlugin;
use theseus::{hydra::init::DeviceLoginSuccess, prelude::*}; use tauri::Manager;
use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("auth") tauri::plugin::Builder::new("auth")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
auth_authenticate_begin_flow, auth_get_default_user,
auth_authenticate_await_completion, auth_set_default_user,
auth_cancel_flow,
auth_refresh,
auth_remove_user, auth_remove_user,
auth_has_user,
auth_users, auth_users,
auth_get_user, auth_get_user,
]) ])
@ -20,47 +19,73 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
/// Authenticate a user with Hydra - part 1 /// Authenticate a user with Hydra - part 1
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at) /// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
#[tauri::command] #[tauri::command]
pub async fn auth_authenticate_begin_flow() -> Result<DeviceLoginSuccess> { pub async fn auth_login(app: tauri::AppHandle) -> Result<Option<Credentials>> {
Ok(auth::authenticate_begin_flow().await?) let flow = minecraft_auth::begin_login().await?;
}
/// Authenticate a user with Hydra - part 2 let start = Utc::now();
/// This completes the authentication flow quasi-synchronously, returning the sign-in credentials
/// (and also adding the credentials to the state)
#[tauri::command]
pub async fn auth_authenticate_await_completion() -> Result<Credentials> {
Ok(auth::authenticate_await_complete_flow().await?)
}
#[tauri::command] if let Some(window) = app.get_window("signin") {
pub async fn auth_cancel_flow() -> Result<()> { window.close()?;
Ok(auth::cancel_flow().await?) }
}
/// Refresh some credentials using Hydra, if needed let window = tauri::WindowBuilder::new(
// invoke('plugin:auth|auth_refresh',user) &app,
#[tauri::command] "signin",
pub async fn auth_refresh(user: uuid::Uuid) -> Result<Credentials> { tauri::WindowUrl::External(flow.redirect_uri.parse().map_err(
Ok(auth::refresh(user).await?) |_| {
} theseus::ErrorKind::OtherError(
"Error parsing auth redirect URL".to_string(),
)
.as_error()
},
)?),
)
.title("Sign into Modrinth")
.build()?;
while (Utc::now() - start) < Duration::minutes(10) {
if window
.url()
.as_str()
.starts_with("https://login.live.com/oauth20_desktop.srf")
{
if let Some((_, code)) =
window.url().query_pairs().find(|x| x.0 == "code")
{
window.close()?;
let val =
minecraft_auth::finish_login(&code.clone(), flow).await?;
return Ok(Some(val));
}
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
window.close()?;
Ok(None)
}
#[tauri::command] #[tauri::command]
pub async fn auth_remove_user(user: uuid::Uuid) -> Result<()> { pub async fn auth_remove_user(user: uuid::Uuid) -> Result<()> {
Ok(auth::remove_user(user).await?) Ok(minecraft_auth::remove_user(user).await?)
} }
/// Check if a user exists in Theseus
// invoke('plugin:auth|auth_has_user',user)
#[tauri::command] #[tauri::command]
pub async fn auth_has_user(user: uuid::Uuid) -> Result<bool> { pub async fn auth_get_default_user() -> Result<Option<uuid::Uuid>> {
Ok(auth::has_user(user).await?) Ok(minecraft_auth::get_default_user().await?)
}
#[tauri::command]
pub async fn auth_set_default_user(user: uuid::Uuid) -> Result<()> {
Ok(minecraft_auth::set_default_user(user).await?)
} }
/// Get a copy of the list of all user credentials /// Get a copy of the list of all user credentials
// invoke('plugin:auth|auth_users',user) // invoke('plugin:auth|auth_users',user)
#[tauri::command] #[tauri::command]
pub async fn auth_users() -> Result<Vec<Credentials>> { pub async fn auth_users() -> Result<Vec<Credentials>> {
Ok(auth::users().await?) Ok(minecraft_auth::users().await?)
} }
/// Get a user from the UUID /// Get a user from the UUID
@ -68,5 +93,5 @@ pub async fn auth_users() -> Result<Vec<Credentials>> {
// invoke('plugin:auth|auth_users',user) // invoke('plugin:auth|auth_users',user)
#[tauri::command] #[tauri::command]
pub async fn auth_get_user(user: uuid::Uuid) -> Result<Credentials> { pub async fn auth_get_user(user: uuid::Uuid) -> Result<Credentials> {
Ok(auth::get_user(user).await?) Ok(minecraft_auth::get_user(user).await?)
} }

View File

@ -35,6 +35,9 @@ pub enum TheseusSerializableError {
#[error("IO error: {0}")] #[error("IO error: {0}")]
IO(#[from] std::io::Error), IO(#[from] std::io::Error),
#[error("Tauri error: {0}")]
Tauri(#[from] tauri::Error),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[error("Callback error: {0}")] #[error("Callback error: {0}")]
Callback(String), Callback(String),
@ -88,9 +91,12 @@ macro_rules! impl_serialize {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
impl_serialize! { impl_serialize! {
IO, IO,
Tauri,
Callback Callback
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
impl_serialize! { impl_serialize! {
IO, IO,
Tauri,
} }

View File

@ -146,6 +146,7 @@ fn main() {
initialize_state, initialize_state,
is_dev, is_dev,
toggle_decorations, toggle_decorations,
api::auth::auth_login,
]); ]);
builder builder

View File

@ -20,6 +20,7 @@ import { get } from '@/helpers/settings'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue' import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue' import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue' import SplashScreen from '@/components/ui/SplashScreen.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator' import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
import { handleError, useNotifications } from '@/store/notifications.js' import { handleError, useNotifications } from '@/store/notifications.js'
import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js' import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js'
@ -40,15 +41,14 @@ import { TauriEvent } from '@tauri-apps/api/event'
import { await_sync, check_safe_loading_bars_complete } from './helpers/state' import { await_sync, check_safe_loading_bars_complete } from './helpers/state'
import { confirm } from '@tauri-apps/api/dialog' import { confirm } from '@tauri-apps/api/dialog'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue' import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue' import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
import { install_from_file } from './helpers/pack' import { install_from_file } from './helpers/pack'
import { useError } from '@/store/error.js'
const themeStore = useTheming() const themeStore = useTheming()
const urlModal = ref(null) const urlModal = ref(null)
const isLoading = ref(true) const isLoading = ref(true)
const videoPlaying = ref(false)
const offline = ref(false) const offline = ref(false)
const showOnboarding = ref(false) const showOnboarding = ref(false)
const nativeDecorations = ref(false) const nativeDecorations = ref(false)
@ -71,7 +71,6 @@ defineExpose({
} = await get() } = await get()
// video should play if the user is not on linux, and has not onboarded // video should play if the user is not on linux, and has not onboarded
os.value = await getOS() os.value = await getOS()
videoPlaying.value = !fully_onboarded && os.value !== 'Linux'
const dev = await isDev() const dev = await isDev()
const version = await getVersion() const version = await getVersion()
showOnboarding.value = !fully_onboarded showOnboarding.value = !fully_onboarded
@ -180,12 +179,19 @@ const isOnBrowse = computed(() => route.path.startsWith('/browse'))
const loading = useLoading() const loading = useLoading()
const notifications = useNotifications() const notifications = useNotifications()
const notificationsWrapper = ref(null) const notificationsWrapper = ref()
watch(notificationsWrapper, () => { watch(notificationsWrapper, () => {
notifications.setNotifs(notificationsWrapper.value) notifications.setNotifs(notificationsWrapper.value)
}) })
const error = useError()
const errorModal = ref()
watch(errorModal, () => {
error.setErrorModal(errorModal.value)
})
document.querySelector('body').addEventListener('click', function (e) { document.querySelector('body').addEventListener('click', function (e) {
let target = e.target let target = e.target
while (target != null) { while (target != null) {
@ -245,15 +251,6 @@ command_listener(async (e) => {
</script> </script>
<template> <template>
<StickyTitleBar v-if="videoPlaying" />
<video
v-if="videoPlaying"
ref="onboardingVideo"
class="video"
src="@/assets/video.mp4"
autoplay
@ended="videoPlaying = false"
/>
<div v-if="failureText" class="failure dark-mode"> <div v-if="failureText" class="failure dark-mode">
<div class="appbar-failure dark-mode"> <div class="appbar-failure dark-mode">
<Button v-if="os != 'MacOS'" icon-only @click="TauriWindow.getCurrent().close()"> <Button v-if="os != 'MacOS'" icon-only @click="TauriWindow.getCurrent().close()">
@ -294,7 +291,7 @@ command_listener(async (e) => {
</Card> </Card>
</div> </div>
</div> </div>
<SplashScreen v-else-if="!videoPlaying && isLoading" app-loading /> <SplashScreen v-else-if="isLoading" app-loading />
<OnboardingScreen v-else-if="showOnboarding" :finish="() => (showOnboarding = false)" /> <OnboardingScreen v-else-if="showOnboarding" :finish="() => (showOnboarding = false)" />
<div v-else class="container"> <div v-else class="container">
<div class="nav-container"> <div class="nav-container">
@ -389,6 +386,7 @@ command_listener(async (e) => {
</div> </div>
<URLConfirmModal ref="urlModal" /> <URLConfirmModal ref="urlModal" />
<Notifications ref="notificationsWrapper" /> <Notifications ref="notificationsWrapper" />
<ErrorModal ref="errorModal" />
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

Binary file not shown.

View File

@ -56,68 +56,22 @@
</Button> </Button>
</Card> </Card>
</transition> </transition>
<Modal ref="loginModal" class="modal" header="Signing in" :noblur="!themeStore.advancedRendering">
<div class="modal-body">
<QrcodeVue :value="loginUrl" class="qr-code" margin="3" size="160" />
<div class="modal-text">
<div class="label">Copy this code</div>
<div class="code-text">
<div class="code">
{{ loginCode }}
</div>
<Button
v-tooltip="'Copy code'"
icon-only
large
color="raised"
@click="() => clipboardWrite(loginCode)"
>
<ClipboardCopyIcon />
</Button>
</div>
<div>And enter it on Microsoft's website to sign in.</div>
<div class="iconified-input">
<LogInIcon />
<input type="text" :value="loginUrl" readonly />
<Button
v-tooltip="'Open link'"
icon-only
color="raised"
@click="() => clipboardWrite(loginUrl)"
>
<GlobeIcon />
</Button>
</div>
</div>
</div>
</Modal>
</template> </template>
<script setup> <script setup>
import { import { Avatar, Button, Card, PlusIcon, TrashIcon, LogInIcon } from 'omorphia'
Avatar,
Button,
Card,
PlusIcon,
TrashIcon,
LogInIcon,
Modal,
GlobeIcon,
ClipboardCopyIcon,
} from 'omorphia'
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue' import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
import { import {
users, users,
remove_user, remove_user,
authenticate_begin_flow, set_default_user,
authenticate_await_completion, login as login_flow,
get_default_user,
} from '@/helpers/auth' } from '@/helpers/auth'
import { get, set } from '@/helpers/settings'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import { useTheming } from '@/store/theme.js'
import { mixpanel_track } from '@/helpers/mixpanel' import { mixpanel_track } from '@/helpers/mixpanel'
import QrcodeVue from 'qrcode.vue'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { handleSevereError } from '@/store/error.js'
defineProps({ defineProps({
mode: { mode: {
@ -129,16 +83,11 @@ defineProps({
const emit = defineEmits(['change']) const emit = defineEmits(['change'])
const loginCode = ref(null) const accounts = ref({})
const defaultUser = ref()
const themeStore = useTheming()
const settings = ref({})
const accounts = ref([])
const loginUrl = ref('')
const loginModal = ref(null)
async function refreshValues() { async function refreshValues() {
settings.value = await get().catch(handleError) defaultUser.value = await get_default_user().catch(handleError)
accounts.value = await users().catch(handleError) accounts.value = await users().catch(handleError)
} }
defineExpose({ defineExpose({
@ -147,46 +96,27 @@ defineExpose({
await refreshValues() await refreshValues()
const displayAccounts = computed(() => const displayAccounts = computed(() =>
accounts.value.filter((account) => settings.value.default_user !== account.id), accounts.value.filter((account) => defaultUser.value !== account.id),
) )
const selectedAccount = computed(() => const selectedAccount = computed(() =>
accounts.value.find((account) => account.id === settings.value.default_user), accounts.value.find((account) => account.id === defaultUser.value),
) )
async function setAccount(account) { async function setAccount(account) {
settings.value.default_user = account.id defaultUser.value = account.id
await set(settings.value).catch(handleError) await set_default_user(account.id).catch(handleError)
emit('change') emit('change')
} }
const clipboardWrite = async (a) => {
navigator.clipboard.writeText(a)
}
async function login() { async function login() {
const loginSuccess = await authenticate_begin_flow().catch(handleError) const loggedIn = await login_flow().catch(handleSevereError)
loginModal.value.show()
loginCode.value = loginSuccess.user_code
loginUrl.value = loginSuccess.verification_uri
await window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: loginSuccess.verification_uri,
},
})
const loggedIn = await authenticate_await_completion().catch(handleError)
loginModal.value.hide()
if (loggedIn) { if (loggedIn) {
await setAccount(loggedIn) await setAccount(loggedIn)
await refreshValues() await refreshValues()
} }
loginModal.value.hide()
mixpanel_track('AccountLogIn') mixpanel_track('AccountLogIn')
} }

View File

@ -0,0 +1,125 @@
<script setup>
import { Modal, XIcon } from 'omorphia'
import { ChatIcon } from '@/assets/icons'
import { ref } from 'vue'
const errorModal = ref()
const error = ref()
const title = ref('An error occurred')
const errorType = ref('unknown')
const supportLink = ref('https://support.modrinth.com')
const metadata = ref({})
defineExpose({
async show(errorVal) {
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
title.value = 'Unable to sign in to Minecraft'
errorType.value = 'minecraft_auth'
supportLink.value =
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
if (errorVal.message.includes('existing connection was forcibly closed')) {
metadata.value.network = true
}
if (errorVal.message.includes('because the target machine actively refused it')) {
metadata.value.hostsFile = true
}
} else {
title.value = 'An error occurred'
errorType.value = 'unknown'
supportLink.value = 'https://support.modrinth.com'
metadata.value = {}
}
error.value = errorVal
errorModal.value.show()
},
})
</script>
<template>
<Modal ref="errorModal" :header="title">
<div class="modal-body">
<div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'">
<p>
Signing into Microsoft account is a complex task for the launchers, and there are a lot
of things can go wrong.
</p>
<template v-if="metadata.network">
<h3>Network issues</h3>
<p>
It looks like there were issues with the Modrinth App connecting to Microsoft's
servers. This is often the result of a poor connection, so we recommend trying again
to see if it works. If issues continue to persist, follow the steps in
<a
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
>
our support article
</a>
to troubleshoot.
</p>
</template>
<template v-else-if="metadata.hostsFile">
<h3>Network issues</h3>
<p>
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
remote server rejected the connection. This may indicate that these services are
blocked by the hosts file. Please visit
<a
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
>
our support article
</a>
for steps on how to fix the issue.
</p>
</template>
<template v-else>
<h3>Make sure you are signing into the right Microsoft account</h3>
<p>
More often than not, this error is caused by you signing into an incorrect Microsoft
account which isn't linked to Minecraft. Double check and try again!
</p>
<h3>Try signing in and launching through the official launcher first</h3>
<p>
If you just bought Minecraft, are coming from the Bedrock Edition world and have never
played Java before, or just subscribed to PC Game Pass, you would need to start the
game at least once using the
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>.
Once you're done, come back here and sign in!
</p>
</template>
<hr />
<p>
If nothing is working and you need help, visit
<a :href="supportLink">our support page</a>
and start a chat using the widget in the bottom right and we will be more than happy to
assist!
</p>
<details>
<summary>Debug info</summary>
{{ error.message ?? error }}
</details>
</template>
<template v-else>
{{ error.message ?? error }}
</template>
</div>
<div class="input-group push-right">
<a :href="supportLink" class="btn" @click="errorModal.hide()"><ChatIcon /> Get support</a>
<button class="btn btn-primary" @click="errorModal.hide()"><XIcon /> Close</button>
</div>
</div>
</Modal>
</template>
<style scoped lang="scss">
.modal-body {
display: flex;
flex-direction: column;
gap: var(--gap-md);
padding: var(--gap-lg);
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="action-groups"> <div class="action-groups">
<a href="https://discord.modrinth.com" class="link"> <a href="https://support.modrinth.com" class="link">
<ChatIcon /> <ChatIcon />
<span> Get support </span> <span> Get support </span>
</a> </a>

View File

@ -1,178 +0,0 @@
<template>
<div ref="button" class="button-base avatar-button" :class="{ highlighted: showDemo }">
<Avatar src="https://launcher-files.modrinth.com/assets/steve_head.png" />
</div>
<transition name="fade">
<div v-if="showDemo" class="card-section">
<Card ref="card" class="fake-account-card expanded highlighted">
<div class="selected account">
<Avatar size="xs" src="https://launcher-files.modrinth.com/assets/steve_head.png" />
<div>
<h4>Modrinth</h4>
<p>Selected</p>
</div>
<Button v-tooltip="'Log out'" icon-only color="raised">
<TrashIcon />
</Button>
</div>
<Button>
<PlusIcon />
Add account
</Button>
</Card>
<slot />
</div>
</transition>
</template>
<script setup>
import { Avatar, Button, Card, PlusIcon, TrashIcon } from 'omorphia'
defineProps({
showDemo: {
type: Boolean,
default: false,
},
})
</script>
<style scoped lang="scss">
.selected {
background: var(--color-brand-highlight);
border-radius: var(--radius-lg);
color: var(--color-contrast);
gap: 1rem;
}
.logged-out {
background: var(--color-bg);
border-radius: var(--radius-lg);
gap: 1rem;
}
.account {
width: max-content;
display: flex;
align-items: center;
text-align: left;
padding: 0.5rem 1rem;
h4,
p {
margin: 0;
}
}
.card-section {
position: absolute;
top: 0.5rem;
left: 5.5rem;
z-index: 9;
display: flex;
flex-direction: column;
}
.fake-account-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
border: 1px solid var(--color-button-bg);
width: max-content;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
&.hidden {
display: none;
}
&.isolated {
position: relative;
left: 0;
top: 0;
}
}
.accounts-title {
font-size: 1.2rem;
font-weight: bolder;
}
.account-group {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.option {
width: calc(100% - 2.25rem);
background: var(--color-raised-bg);
color: var(--color-base);
box-shadow: none;
img {
margin-right: 0.5rem;
}
}
.icon {
--size: 1.5rem !important;
}
.account-row {
display: flex;
flex-direction: row;
gap: 0.5rem;
vertical-align: center;
justify-content: space-between;
padding-right: 1rem;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.avatar-button {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--color-base);
background-color: var(--color-raised-bg);
border-radius: var(--radius-md);
width: 100%;
text-align: left;
&.expanded {
border: 1px solid var(--color-button-bg);
padding: 1rem;
}
}
.avatar-text {
margin: auto 0 auto 0.25rem;
display: flex;
flex-direction: column;
}
.text {
width: 6rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.accounts-text {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
}
</style>

View File

@ -1,265 +0,0 @@
<template>
<div class="action-groups">
<Button color="danger" outline @click="exit">
<LogOutIcon />
Exit tutorial
</Button>
<Button v-if="showDownload" ref="infoButton" icon-only class="icon-button show-card-icon">
<DownloadIcon />
</Button>
<div v-if="showRunning" class="status highlighted">
<span class="circle running" />
<div ref="profileButton" class="running-text">Example Modpack</div>
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop">
<StopCircleIcon />
</Button>
<Button v-tooltip="'View logs'" icon-only class="icon-button">
<TerminalSquareIcon />
</Button>
</div>
<div v-else class="status">
<span class="circle stopped" />
<span class="running-text"> No running instances </span>
</div>
</div>
<transition name="download">
<div v-if="showDownload" class="info-section">
<Card ref="card" class="highlighted info-card">
<h3 class="info-title">New Modpack</h3>
<ProgressBar :progress="50" />
<div class="row">50% Downloading modpack</div>
</Card>
<slot name="download" />
</div>
</transition>
<transition name="running">
<div v-if="showRunning" class="info-section">
<slot name="running" />
</div>
</transition>
</template>
<script setup>
import {
Button,
DownloadIcon,
Card,
StopCircleIcon,
TerminalSquareIcon,
LogOutIcon,
} from 'omorphia'
import ProgressBar from '@/components/ui/ProgressBar.vue'
defineProps({
showDownload: {
type: Boolean,
default: false,
},
showRunning: {
type: Boolean,
default: false,
},
exit: {
type: Function,
required: true,
},
})
</script>
<style scoped lang="scss">
.action-groups {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
}
.arrow {
transition: transform 0.2s ease-in-out;
display: flex;
align-items: center;
&.rotate {
transform: rotate(180deg);
}
}
.status {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
border-radius: var(--radius-md);
border: 1px solid var(--color-button-bg);
padding: var(--gap-sm) var(--gap-lg);
}
.running-text {
display: flex;
flex-direction: row;
gap: var(--gap-xs);
white-space: nowrap;
overflow: hidden;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none;
&.clickable:hover {
cursor: pointer;
}
}
.circle {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
display: inline-block;
margin-right: 0.25rem;
&.running {
background-color: var(--color-brand);
}
&.stopped {
background-color: var(--color-base);
}
}
.icon-button {
background-color: rgba(0, 0, 0, 0);
box-shadow: none;
width: 1.25rem !important;
height: 1.25rem !important;
&.stop {
--text-color: var(--color-red) !important;
}
}
.info-section {
position: absolute;
top: 3.5rem;
right: 0.75rem;
z-index: 9;
display: flex;
flex-direction: column;
}
.info-card {
width: 20rem;
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-raised);
display: flex;
flex-direction: column;
gap: 1rem;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg);
&.hidden {
transform: translateY(-100%);
}
}
.loading-option {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 0;
:hover {
background-color: var(--color-raised-bg-hover);
}
}
.loading-text {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
.row {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
}
.loading-icon {
width: 2.25rem;
height: 2.25rem;
display: block;
:deep(svg) {
left: 1rem;
width: 2.25rem;
height: 2.25rem;
}
}
.show-card-icon {
color: var(--color-brand);
}
.download-enter-active,
.download-leave-active {
transition: opacity 0.3s ease;
}
.download-enter-from,
.download-leave-to {
opacity: 0;
}
.progress-bar {
width: 100%;
}
.info-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
margin: 0;
padding: 0;
}
.info-title {
margin: 0;
}
.profile-button {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
width: 100%;
background-color: var(--color-raised-bg);
box-shadow: none;
.text {
margin-right: auto;
}
}
.profile-card {
position: absolute;
top: 3.5rem;
right: 0.5rem;
z-index: 9;
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-raised);
display: flex;
flex-direction: column;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg);
padding: var(--gap-md);
&.hidden {
transform: translateY(-100%);
}
}
</style>

View File

@ -1,222 +0,0 @@
<script setup>
import { ref } from 'vue'
import { Card, DropdownSelect, SearchIcon, XIcon, Button, Avatar } from 'omorphia'
const search = ref('')
const group = ref('Category')
const filters = ref('All profiles')
const sortBy = ref('Name')
defineProps({
showFilters: {
type: Boolean,
default: false,
},
showInstances: {
type: Boolean,
default: false,
},
})
</script>
<template>
<Card class="header" :class="{ highlighted: showFilters }">
<div class="iconified-input">
<SearchIcon />
<input v-model="search" type="text" placeholder="Search" class="search-input" />
<Button @click="() => (search = '')">
<XIcon />
</Button>
</div>
<div class="labeled_button">
<span>Sort by</span>
<DropdownSelect
v-model="sortBy"
class="sort-dropdown"
name="Sort Dropdown"
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
placeholder="Select..."
/>
</div>
<div class="labeled_button">
<span>Filter by</span>
<DropdownSelect
v-model="filters"
class="filter-dropdown"
name="Filter Dropdown"
:options="['All profiles', 'Custom instances', 'Downloaded modpacks']"
placeholder="Select..."
/>
</div>
<div class="labeled_button">
<span>Group by</span>
<DropdownSelect
v-model="group"
class="group-dropdown"
name="Group dropdown"
:options="['Category', 'Loader', 'Game version', 'None']"
placeholder="Select..."
/>
</div>
</Card>
<div class="row">
<section class="instances">
<Card
v-for="project in 20"
:key="project"
class="instance-card-item button-base"
:class="{ highlighted: project === 1 && showInstance }"
>
<Avatar
size="sm"
src="https://launcher-files.modrinth.com/assets/default_profile.png"
alt="Mod card"
class="mod-image"
/>
<div class="project-info">
<p class="title">Example Profile</p>
<p class="description">Forge/Fabric 1.20.1</p>
</div>
</Card>
<slot />
</section>
</div>
</template>
<style lang="scss" scoped>
.row {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
padding: 1rem;
.divider {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 1rem;
margin-bottom: 1rem;
p {
margin: 0;
font-size: 1rem;
white-space: nowrap;
color: var(--color-contrast);
}
hr {
background-color: var(--color-gray);
height: 1px;
width: 100%;
border: none;
}
}
}
.header {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
align-items: inherit;
margin: 1rem 1rem 0 !important;
padding: 1rem;
width: calc(100% - 2rem);
.iconified-input {
flex-grow: 1;
input {
min-width: 100%;
}
}
.sort-dropdown {
width: 10rem;
}
.filter-dropdown {
width: 15rem;
}
.group-dropdown {
width: 10rem;
}
.labeled_button {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
}
.instances {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
width: 100%;
gap: 1rem;
margin-right: auto;
scroll-behavior: smooth;
}
.instance {
position: relative;
}
.instance-card-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
padding: var(--gap-md);
transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */
margin-bottom: 0;
.mod-image {
--size: 100%;
width: 100% !important;
height: auto !important;
max-width: unset !important;
max-height: unset !important;
aspect-ratio: 1 / 1 !important;
}
.project-info {
margin-top: 1rem;
width: 100%;
.title {
color: var(--color-contrast);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
margin: 0;
font-weight: 600;
font-size: 1rem;
line-height: 110%;
display: inline-block;
}
.description {
color: var(--color-base);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 500;
font-size: 0.775rem;
line-height: 125%;
margin: 0.25rem 0 0;
text-transform: capitalize;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
</style>

View File

@ -1,376 +0,0 @@
<script setup>
import {
DownloadIcon,
ChevronRightIcon,
formatNumber,
CalendarIcon,
HeartIcon,
Avatar,
Card,
} from 'omorphia'
import { onMounted, onUnmounted, ref } from 'vue'
const modsRow = ref(null)
const rows = ref(null)
const maxInstancesPerRow = ref(0)
const maxProjectsPerRow = ref(0)
const calculateCardsPerRow = () => {
// Calculate how many cards fit in one row
const containerWidth = rows.value[0].clientWidth
// Convert container width from pixels to rem
const containerWidthInRem =
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 1) / 11)
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 1) / 17)
}
onMounted(() => {
calculateCardsPerRow()
window.addEventListener('resize', calculateCardsPerRow)
})
onUnmounted(() => {
window.removeEventListener('resize', calculateCardsPerRow)
})
defineProps({
showInstance: {
type: Boolean,
default: false,
},
})
</script>
<template>
<div class="content">
<div
v-for="(row, index) in ['Jump back in', 'Popular modpacks', 'Popular mods']"
ref="rows"
:key="row"
class="row"
>
<div class="header">
<p>{{ row }}</p>
<ChevronRightIcon />
</div>
<section v-if="index < 1" ref="modsRow" class="instances">
<Card
v-for="project in maxInstancesPerRow"
:key="project"
class="instance-card-item button-base"
:class="{ highlighted: showInstance }"
>
<Avatar
size="sm"
src="https://launcher-files.modrinth.com/assets/default_profile.png"
alt="Mod card"
class="mod-image"
/>
<div class="project-info">
<p class="title">Example Profile</p>
<p class="description">Forge/Fabric 1.20.1</p>
</div>
</Card>
</section>
<section v-else ref="modsRow" class="projects">
<div v-for="project in maxProjectsPerRow" :key="project" class="wrapper">
<Card class="project-card button-base" :class="{ highlighted: showInstance }">
<div
class="banner no-image"
:style="{
'background-image': `url(https://launcher-files.modrinth.com/assets/maze-bg.png)`,
}"
>
<div class="badges">
<div class="badge">
<DownloadIcon />
{{ formatNumber(69420) }}
</div>
<div class="badge">
<HeartIcon />
{{ formatNumber(69) }}
</div>
<div class="badge">
<CalendarIcon />
Today
</div>
</div>
<div
class="badges-wrapper no-image"
:style="{
background:
'linear-gradient(rgba(' +
[27, 217, 106, 0.03].join(',') +
'), 65%, rgba(' +
[27, 217, 106, 0.3].join(',') +
'))',
}"
></div>
</div>
<Avatar
class="icon"
size="sm"
src="https://launcher-files.modrinth.com/assets/default_profile.png"
/>
<div class="title">
<div class="title-text">Example Project</div>
<div class="author">by Modrinth</div>
</div>
<div class="description">
An example project hangin on the Rinth. Very cool project, its probably on Forge and
Fabric. Probably has a 401k and a family.
</div>
</Card>
</div>
</section>
</div>
</div>
</template>
<style lang="scss" scoped>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 1rem;
gap: 1rem;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
background: transparent;
}
}
.row {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
min-width: 100%;
&:nth-child(even) {
background: var(--color-bg);
}
.header {
width: 100%;
margin-bottom: 1rem;
gap: var(--gap-xs);
display: flex;
flex-direction: row;
align-items: center;
p {
margin: 0;
font-size: var(--font-size-lg);
font-weight: bolder;
white-space: nowrap;
color: var(--color-contrast);
}
svg {
height: 1.5rem;
width: 1.5rem;
color: var(--color-contrast);
}
}
.instances {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-gap: 1rem;
width: 100%;
}
.projects {
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
grid-gap: 1rem;
.item {
width: 100%;
max-width: 100%;
}
}
}
.loading-indicator {
width: 2.5rem !important;
height: 2.5rem !important;
svg {
width: 2.5rem !important;
height: 2.5rem !important;
}
}
.instance {
position: relative;
}
.instance-card-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
padding: var(--gap-md);
transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */
margin-bottom: 0;
.mod-image {
--size: 100%;
width: 100% !important;
height: auto !important;
max-width: unset !important;
max-height: unset !important;
aspect-ratio: 1 / 1 !important;
}
.project-info {
margin-top: 1rem;
width: 100%;
.title {
color: var(--color-contrast);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
margin: 0;
font-weight: 600;
font-size: 1rem;
line-height: 110%;
display: inline-block;
}
.description {
color: var(--color-base);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 500;
font-size: 0.775rem;
line-height: 125%;
margin: 0.25rem 0 0;
text-transform: capitalize;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.wrapper {
position: relative;
aspect-ratio: 1;
&:hover {
.install:enabled {
opacity: 1;
}
}
}
.project-card {
display: grid;
grid-gap: 1rem;
grid-template:
'. . . .' 0
'. icon title .' 3rem
'banner banner banner banner' auto
'. description description .' 3.5rem
'. . . .' 0 / 0 3rem minmax(0, 1fr) 0;
max-width: 100%;
height: 100%;
padding: 0;
margin: 0;
.icon {
grid-area: icon;
}
.title {
max-width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
grid-area: title;
white-space: nowrap;
.title-text {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
font-size: var(--font-size-md);
font-weight: bold;
}
}
.author {
font-size: var(--font-size-sm);
grid-area: author;
}
.banner {
grid-area: banner;
background-size: cover;
background-position: center;
position: relative;
.badges-wrapper {
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left: 0;
mix-blend-mode: hard-light;
}
.badges {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: var(--gap-sm);
gap: var(--gap-xs);
display: flex;
z-index: 1;
flex-direction: row;
justify-content: flex-end;
align-items: flex-end;
}
}
.description {
grid-area: description;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
}
.badge {
background-color: var(--color-raised-bg);
font-size: var(--font-size-xs);
padding: var(--gap-xs) var(--gap-sm);
border-radius: var(--radius-sm);
svg {
width: 1rem;
height: 1rem;
margin-right: var(--gap-xs);
}
}
</style>

View File

@ -1,496 +0,0 @@
<script setup>
import { computed, readonly, ref } from 'vue'
import {
Avatar,
Button,
CalendarIcon,
Card,
Categories,
Checkbox,
ClearIcon,
ClientIcon,
DownloadIcon,
DropdownSelect,
EnvironmentIndicator,
formatCategory,
formatCategoryHeader,
formatNumber,
HeartIcon,
NavRow,
Pagination,
Promotion,
SearchFilter,
SearchIcon,
ServerIcon,
StarIcon,
XIcon,
} from 'omorphia'
import Multiselect from 'vue-multiselect'
import { handleError } from '@/store/state'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import SplashScreen from '@/components/ui/SplashScreen.vue'
const loading = ref(false)
const query = ref('')
const facets = ref([])
const orFacets = ref([])
const selectedVersions = ref([])
const onlyOpenSource = ref(false)
const showSnapshots = ref(false)
const selectedEnvironments = ref([])
const sortTypes = readonly([
{ display: 'Relevance', name: 'relevance' },
{ display: 'Download count', name: 'downloads' },
{ display: 'Follow count', name: 'follows' },
{ display: 'Recently published', name: 'newest' },
{ display: 'Recently updated', name: 'updated' },
])
const sortType = ref(sortTypes[0])
const maxResults = ref(20)
const currentPage = ref(1)
const projectType = ref('modpack')
const searchWrapper = ref(null)
const sortedCategories = computed(() => {
const values = new Map()
for (const category of categories.value.filter((cat) => cat.project_type === 'mod')) {
if (!values.has(category.header)) {
values.set(category.header, [])
}
values.get(category.header).push(category)
}
return values
})
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories().catch(handleError).then(ref),
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
])
const pageCount = ref(1)
const selectableProjectTypes = computed(() => {
return [
{ label: 'Shaders', href: `` },
{ label: 'Resource Packs', href: `` },
{ label: 'Data Packs', href: `` },
{ label: 'Mods', href: '' },
{ label: 'Modpacks', href: '' },
]
})
defineProps({
showSearch: {
type: Boolean,
default: false,
},
})
</script>
<template>
<div class="search-container">
<aside class="filter-panel">
<Card class="search-panel-card" :class="{ highlighted: showSearch }">
<Button role="button" disabled> <ClearIcon /> Clear Filters </Button>
<div class="loaders">
<h2>Loaders</h2>
<div
v-for="loader in loaders.filter(
(l) =>
(projectType !== 'mod' && l.supported_project_types?.includes(projectType)) ||
(projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name)),
)"
:key="loader"
>
<SearchFilter
:active-filters="orFacets"
:icon="loader.icon"
:display-name="formatCategory(loader.name)"
:facet-name="`categories:${encodeURIComponent(loader.name)}`"
class="filter-checkbox"
/>
</div>
</div>
<div class="versions">
<h2>Minecraft versions</h2>
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
<multiselect
v-model="selectedVersions"
:options="
showSnapshots
? availableGameVersions.map((x) => x.version)
: availableGameVersions
.filter((it) => it.version_type === 'release')
.map((x) => x.version)
"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-search-on-select="false"
:show-labels="false"
placeholder="Choose versions..."
/>
</div>
<div
v-for="categoryList in Array.from(sortedCategories)"
:key="categoryList[0]"
class="categories"
>
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
<div v-for="category in categoryList[1]" :key="category.name">
<SearchFilter
:active-filters="facets"
:icon="category.icon"
:display-name="formatCategory(category.name)"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
class="filter-checkbox"
/>
</div>
</div>
<div v-if="projectType !== 'datapack'" class="environment">
<h2>Environments</h2>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Client"
facet-name="client"
class="filter-checkbox"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Server"
facet-name="server"
class="filter-checkbox"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</div>
<div class="open-source">
<h2>Open source</h2>
<Checkbox v-model="onlyOpenSource" label="Open source only" class="filter-checkbox" />
</div>
</Card>
</aside>
<div ref="searchWrapper" class="search">
<Promotion class="promotion" :external="false" query-param="?r=launcher" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" />
</Card>
<Card class="search-panel-container" :class="{ highlighted: showSearch }">
<div class="iconified-input">
<SearchIcon aria-hidden="true" />
<input
v-model="query"
autocomplete="off"
type="text"
:placeholder="`Search ${projectType}s...`"
/>
<Button @click="() => (query = '')">
<XIcon />
</Button>
</div>
<div class="inline-option">
<span>Sort by</span>
<DropdownSelect
v-model="sortType"
name="Sort by"
:options="sortTypes"
:display-name="(option) => option?.display"
/>
</div>
<div class="inline-option">
<span>Show per page</span>
<DropdownSelect
v-model="maxResults"
name="Max results"
:options="[5, 10, 15, 20, 50, 100]"
:default-value="maxResults"
:model-value="maxResults"
class="limit-dropdown"
/>
</div>
</Card>
<Pagination :page="currentPage" :count="pageCount" class="pagination-before" />
<SplashScreen v-if="loading" />
<section v-else class="project-list display-mode--list instance-results" role="list">
<Card v-for="project in 20" :key="project" class="search-card button-base">
<div class="icon">
<Avatar
src="https://launcher-files.modrinth.com/assets/default_profile.png"
size="md"
class="search-icon"
/>
</div>
<div class="content-wrapper">
<div class="title joined-text">
<h2>Example Modpack</h2>
<span>by Modrinth</span>
</div>
<div class="description">
A very cool project that does cool project things that you can your friends can do.
</div>
<div class="tags">
<Categories
:categories="
categories
.filter((cat) => cat.project_type === projectType)
.slice(project / 2, project / 2 + 3)
"
:type="modpack"
>
<EnvironmentIndicator
:type-only="true"
:client-side="true"
:server-side="true"
type="modpack"
:search="true"
/>
</Categories>
</div>
</div>
<div class="stats button-group">
<div v-if="featured" class="badge">
<StarIcon />
Featured
</div>
<div class="badge">
<DownloadIcon />
{{ formatNumber(420) }}
</div>
<div class="badge">
<HeartIcon />
{{ formatNumber(69) }}
</div>
<div class="badge">
<CalendarIcon />
A minute ago
</div>
</div>
</Card>
</section>
<pagination :page="currentPage" :count="pageCount" class="pagination-after" />
</div>
</div>
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
.small-instance {
min-height: unset !important;
.instance {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
.title {
font-weight: 600;
color: var(--color-contrast);
}
}
.small-instance_info {
display: flex;
flex-direction: column;
gap: 0.25rem;
justify-content: space-between;
padding: 0.25rem 0;
}
}
.filter-checkbox {
margin-bottom: 0.3rem;
font-size: 1rem;
svg {
display: flex;
align-self: center;
justify-self: center;
}
button.checkbox {
border: none;
}
}
</style>
<style lang="scss" scoped>
.project-type-dropdown {
width: 100% !important;
}
.promotion {
margin-top: 1rem;
}
.project-type-container {
display: flex;
flex-direction: column;
width: 100%;
}
.search-panel-card {
display: flex;
flex-direction: column;
margin-bottom: 0 !important;
min-height: min-content !important;
}
.iconified-input {
input {
max-width: none !important;
flex-basis: auto;
}
}
.search-panel-container {
display: inline-flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
width: 100%;
padding: 1rem !important;
white-space: nowrap;
gap: 1rem;
.inline-option {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
.sort-dropdown {
max-width: 12.25rem;
}
.limit-dropdown {
width: 5rem;
}
}
.iconified-input {
flex-grow: 1;
}
.filter-panel {
button {
display: flex;
align-items: center;
justify-content: space-evenly;
svg {
margin-right: 0.4rem;
}
}
}
}
.search-container {
display: flex;
.filter-panel {
position: fixed;
width: 20rem;
padding: 1rem 0.5rem 1rem 1rem;
display: flex;
flex-direction: column;
height: fit-content;
min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem);
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
background: transparent;
}
h2 {
color: var(--color-contrast);
margin-top: 1rem;
margin-bottom: 0.5rem;
font-size: 1.16rem;
}
}
.search {
scroll-behavior: smooth;
margin: 0 1rem 0.5rem 20.5rem;
width: calc(100% - 20.5rem);
.loading {
margin: 2rem;
text-align: center;
}
}
}
.search-card {
margin-bottom: 0;
display: grid;
grid-template-columns: 6rem auto 7rem;
gap: 0.75rem;
padding: 1rem;
&:active:not(&:disabled) {
scale: 0.98 !important;
}
}
.joined-text {
display: inline-flex;
flex-wrap: wrap;
flex-direction: row;
column-gap: 0.5rem;
align-items: baseline;
overflow: hidden;
text-overflow: ellipsis;
h2 {
margin-bottom: 0 !important;
word-wrap: break-word;
overflow-wrap: anywhere;
}
}
.button-group {
display: inline-flex;
flex-direction: row;
gap: 0.5rem;
align-items: flex-start;
flex-wrap: wrap;
justify-content: flex-start;
}
.icon {
grid-column: 1;
grid-row: 1;
align-self: center;
height: 6rem;
}
.content-wrapper {
display: flex;
justify-content: space-between;
grid-column: 2 / 4;
flex-direction: column;
grid-row: 1;
gap: 0.5rem;
.description {
word-wrap: break-word;
overflow-wrap: anywhere;
}
}
.stats {
grid-column: 1 / 3;
grid-row: 2;
justify-self: stretch;
align-self: start;
}
</style>

View File

@ -1,270 +0,0 @@
<script setup>
import { Card, Slider, DropdownSelect, Toggle } from 'omorphia'
import JavaSelector from '@/components/ui/JavaSelector.vue'
const pageOptions = ['Home', 'Library']
</script>
<template>
<div class="settings-page">
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Display</span>
</h3>
</div>
<div class="adjacent-input">
<label for="theme">
<span class="label__title">Color theme</span>
<span class="label__description">Change the global launcher color theme.</span>
</label>
<DropdownSelect
id="theme"
name="Theme dropdown"
:options="['Dark']"
:disabled="true"
:default-value="'dark'"
class="theme-dropdown disable-children"
/>
</div>
<div class="adjacent-input">
<label for="collapsed-nav">
<span class="label__title">Collapsed navigation mode</span>
<span class="label__description"
>Change the style of the side navigation bar to a compact version.</span
>
</label>
<Toggle id="collapsed-nav" :checked="false" :disabled="true" />
</div>
<div class="adjacent-input">
<label for="advanced-rendering">
<span class="label__title">Advanced rendering</span>
<span class="label__description">
Enables advanced rendering such as blur effects that may cause performance issues
without hardware-accelerated rendering.
</span>
</label>
<Toggle id="advanced-rendering" :checked="true" :disabled="true" />
</div>
<div class="adjacent-input">
<label for="minimize-launcher">
<span class="label__title">Minimize launcher</span>
<span class="label__description"
>Minimize the launcher when a Minecraft process starts.</span
>
</label>
<Toggle id="minimize-launcher" :checked="false" :disabled="true" />
</div>
<div class="opening-page">
<label for="opening-page">
<span class="label__title">Default landing page</span>
<span class="label__description">Change the page to which the launcher opens on.</span>
</label>
<DropdownSelect
id="opening-page"
name="Opening page dropdown"
:options="pageOptions"
default-value="Home"
class="opening-page"
:disabled="true"
/>
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Resource management</span>
</h3>
</div>
<div class="adjacent-input">
<label for="max-downloads">
<span class="label__title">Maximum concurrent downloads</span>
<span class="label__description"
>The maximum amount of files the launcher can download at the same time. Set this to a
lower value if you have a poor internet connection.</span
>
</label>
<Slider id="max-downloads" :min="1" :max="10" :step="1" :disabled="true" />
</div>
<div class="adjacent-input">
<label for="max-writes">
<span class="label__title">Maximum concurrent writes</span>
<span class="label__description"
>The maximum amount of files the launcher can write to the disk at once. Set this to a
lower value if you are frequently getting I/O errors.</span
>
</label>
<Slider id="max-writes" :min="1" :max="50" :step="1" :disabled="true" />
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Privacy</span>
</h3>
</div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Disable analytics</span>
<span class="label__description">
Modrinth collects anonymized analytics and usage data to improve our user experience and
customize your experience. Opting out will disable this data collection.
</span>
</label>
<Toggle id="opt-out-analytics" :disabled="true" />
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Java settings</span>
</h3>
</div>
<label for="java-17">
<span class="label__title">Java 17 location</span>
</label>
<JavaSelector id="java-17" :version="17" model-value="" :disabled="true" />
<label for="java-8">
<span class="label__title">Java 8 location</span>
</label>
<JavaSelector id="java-8" :version="8" model-value="" :disabled="true" />
<hr class="card-divider" />
<label for="java-args">
<span class="label__title">Java arguments</span>
</label>
<input
id="java-args"
autocomplete="off"
type="text"
class="installation-input"
placeholder="Enter java arguments..."
:disabled="true"
/>
<label for="env-vars">
<span class="label__title">Environmental variables</span>
</label>
<input
id="env-vars"
autocomplete="off"
type="text"
class="installation-input"
placeholder="Enter environmental variables..."
:disabled="true"
/>
<hr class="card-divider" />
<div class="adjacent-input">
<label for="max-memory">
<span class="label__title">Java memory</span>
<span class="label__description">
The memory allocated to each instance when it is ran.
</span>
</label>
<Slider id="max-memory" :min="256" :max="10256" :step="1" unit="mb" :disabled="true" />
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Hooks</span>
</h3>
</div>
<div class="adjacent-input">
<label for="pre-launch">
<span class="label__title">Pre launch</span>
<span class="label__description"> Ran before the instance is launched. </span>
</label>
<input
id="pre-launch"
autocomplete="off"
type="text"
placeholder="Enter pre-launch command..."
:disabled="true"
/>
</div>
<div class="adjacent-input">
<label for="wrapper">
<span class="label__title">Wrapper</span>
<span class="label__description"> Wrapper command for launching Minecraft. </span>
</label>
<input
id="wrapper"
autocomplete="off"
type="text"
placeholder="Enter wrapper command..."
:disabled="true"
/>
</div>
<div class="adjacent-input">
<label for="post-exit">
<span class="label__title">Post exit</span>
<span class="label__description"> Ran after the game closes. </span>
</label>
<input
id="post-exit"
autocomplete="off"
type="text"
placeholder="Enter post-exit command..."
:disabled="true"
/>
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Window size</span>
</h3>
</div>
<div class="adjacent-input">
<label for="width">
<span class="label__title">Width</span>
<span class="label__description"> The width of the game window when launched. </span>
</label>
<input
id="width"
autocomplete="off"
type="number"
placeholder="Enter width..."
:disabled="true"
/>
</div>
<div class="adjacent-input">
<label for="height">
<span class="label__title">Height</span>
<span class="label__description"> The height of the game window when launched. </span>
</label>
<input
id="height"
autocomplete="off"
type="number"
class="input"
placeholder="Enter height..."
:disabled="true"
/>
</div>
</Card>
</div>
</template>
<style lang="scss" scoped>
.settings-page {
margin: 1rem;
}
.installation-input {
width: 100% !important;
flex-grow: 1;
}
.theme-dropdown {
text-transform: capitalize;
}
.card-divider {
margin: 1rem 0;
}
.disable-children * {
pointer-events: none;
}
</style>

View File

@ -1,293 +0,0 @@
<script setup>
import {
Button,
Card,
Checkbox,
Chips,
XIcon,
FolderOpenIcon,
FolderSearchIcon,
UpdatedIcon,
} from 'omorphia'
import { ref } from 'vue'
import {
get_default_launcher_path,
get_importable_instances,
import_instance,
} from '@/helpers/import.js'
import { open } from '@tauri-apps/api/dialog'
import { handleError } from '@/store/state.js'
const props = defineProps({
nextPage: {
type: Function,
required: true,
},
prevPage: {
type: Function,
required: true,
},
})
const profiles = ref(
new Map([
['MultiMC', []],
['GDLauncher', []],
['ATLauncher', []],
['Curseforge', []],
['PrismLauncher', []],
]),
)
const loading = ref(false)
const selectedProfileType = ref('MultiMC')
const profileOptions = ref([
{ name: 'MultiMC', path: '' },
{ name: 'GDLauncher', path: '' },
{ name: 'ATLauncher', path: '' },
{ name: 'Curseforge', path: '' },
{ name: 'PrismLauncher', path: '' },
])
// Attempt to get import profiles on default paths
const promises = profileOptions.value.map(async (option) => {
const path = await get_default_launcher_path(option.name).catch(handleError)
if (!path || path === '') return
// Try catch to allow failure and simply ignore default path attempt
try {
const instances = await get_importable_instances(option.name, path)
if (!instances) return
profileOptions.value.find((profile) => profile.name === option.name).path = path
profiles.value.set(
option.name,
instances.map((name) => ({ name, selected: false })),
)
} catch (error) {
// Allow failure silently
}
})
Promise.all(promises)
const selectLauncherPath = async () => {
selectedProfileType.value.path = await open({ multiple: false, directory: true })
if (selectedProfileType.value.path) {
await reload()
}
}
const reload = async () => {
const instances = await get_importable_instances(
selectedProfileType.value.name,
selectedProfileType.value.path,
).catch(handleError)
profiles.value.set(
selectedProfileType.value.name,
instances.map((name) => ({ name, selected: false })),
)
}
const setPath = () => {
profileOptions.value.find((profile) => profile.name === selectedProfileType.value.name).path =
selectedProfileType.value.path
}
const next = async () => {
loading.value = true
for (const launcher of Array.from(profiles.value.entries()).map(([launcher, profiles]) => ({
launcher,
path: profileOptions.value.find((option) => option.name === launcher).path,
profiles,
}))) {
for (const profile of launcher.profiles.filter((profile) => profile.selected)) {
await import_instance(launcher.launcher, launcher.path, profile.name)
.catch(handleError)
.then(() => console.log(`Successfully Imported ${profile.name} from ${launcher.launcher}`))
}
}
loading.value = false
props.nextPage()
}
</script>
<template>
<Card>
<h2>Importing external profiles</h2>
<Chips
v-model="selectedProfileType"
:items="profileOptions"
:format-label="(profile) => profile?.name"
/>
<div class="path-selection">
<h3>{{ selectedProfileType.name }} path</h3>
<div class="path-input">
<div class="iconified-input">
<FolderOpenIcon />
<input
v-model="selectedProfileType.path"
type="text"
placeholder="Path to launcher"
@change="setPath"
/>
<Button @click="() => (selectedLauncherPath = '')">
<XIcon />
</Button>
</div>
<Button icon-only @click="selectLauncherPath">
<FolderSearchIcon />
</Button>
<Button icon-only @click="reload">
<UpdatedIcon />
</Button>
</div>
</div>
<div class="table">
<div class="table-head table-row">
<div class="toggle-all table-cell">
<Checkbox
class="select-checkbox"
:model-value="profiles.get(selectedProfileType.name)?.every((child) => child.selected)"
@update:model-value="
(newValue) =>
profiles
.get(selectedProfileType.name)
?.forEach((child) => (child.selected = newValue))
"
/>
</div>
<div class="name-cell table-cell">Profile name</div>
</div>
<div
v-if="
profiles.get(selectedProfileType.name) &&
profiles.get(selectedProfileType.name).length > 0
"
class="table-content"
>
<div
v-for="(profile, index) in profiles.get(selectedProfileType.name)"
:key="index"
class="table-row"
>
<div class="checkbox-cell table-cell">
<Checkbox v-model="profile.selected" class="select-checkbox" />
</div>
<div class="name-cell table-cell">
{{ profile.name }}
</div>
</div>
</div>
<div v-else class="table-content empty">No profiles found</div>
</div>
<div class="button-row">
<Button class="transparent" @click="prevPage"> Back </Button>
<Button
:disabled="
loading ||
!Array.from(profiles.values())
.flatMap((e) => e)
.some((e) => e.selected)
"
color="primary"
@click="next"
>
{{
loading
? 'Importing...'
: Array.from(profiles.values())
.flatMap((e) => e)
.some((e) => e.selected)
? `Import ${
Array.from(profiles.values())
.flatMap((e) => e)
.filter((e) => e.selected).length
} profiles`
: 'Select profiles to import'
}}
</Button>
<Button class="transparent" @click="nextPage"> Next </Button>
</div>
</Card>
</template>
<style scoped lang="scss">
.card {
padding: var(--gap-xl);
min-height: unset;
overflow-y: auto;
}
.path-selection {
padding: var(--gap-xl);
background-color: var(--color-bg);
border-radius: var(--radius-lg);
display: flex;
flex-direction: column;
margin-bottom: var(--gap-md);
gap: var(--gap-md);
h3 {
margin: 0;
}
.path-input {
display: flex;
align-items: center;
width: 100%;
flex-direction: row;
gap: var(--gap-sm);
.iconified-input {
flex-grow: 1;
:deep(input) {
width: 100%;
flex-basis: auto;
}
}
}
}
.table {
border: 1px solid var(--color-bg);
margin-bottom: var(--gap-md);
}
.table-row {
grid-template-columns: min-content auto;
}
.table-content {
max-height: calc(5 * (18px + 2rem));
height: calc(5 * (18px + 2rem));
overflow-y: auto;
}
.select-checkbox {
button.checkbox {
border: none;
}
}
.button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--gap-md);
.transparent {
padding: var(--gap-sm) 0;
}
}
.empty {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bolder;
color: var(--color-contrast);
}
</style>

View File

@ -1,18 +1,11 @@
<script setup> <script setup>
import { Button, LogInIcon, Modal, ClipboardCopyIcon, GlobeIcon, Card } from 'omorphia' import { Button, LogInIcon, Card } from 'omorphia'
import { authenticate_await_completion, authenticate_begin_flow } from '@/helpers/auth.js' import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import mixpanel from 'mixpanel-browser' import mixpanel from 'mixpanel-browser'
import { get, set } from '@/helpers/settings.js'
import { ref } from 'vue' import { ref } from 'vue'
import QrcodeVue from 'qrcode.vue' import { handleSevereError } from '@/store/error.js'
const loading = ref(false)
const themeStore = useTheming()
const loginUrl = ref(null)
const loginModal = ref()
const loginCode = ref(null)
const finalizedLogin = ref(false)
const props = defineProps({ const props = defineProps({
nextPage: { nextPage: {
@ -26,42 +19,21 @@ const props = defineProps({
}) })
async function login() { async function login() {
const loginSuccess = await authenticate_begin_flow().catch(handleError) try {
loginUrl.value = loginSuccess.verification_uri loading.value = true
loginCode.value = loginSuccess.user_code const loggedIn = await login_flow()
loginModal.value.show()
await window.__TAURI_INVOKE__('tauri', { if (loggedIn) {
__tauriModule: 'Shell', await set_default_user(loggedIn.id).catch(handleError)
message: { }
cmd: 'open',
path: loginSuccess.verification_uri,
},
})
const loggedIn = await authenticate_await_completion().catch(handleError) await mixpanel.track('AccountLogIn')
loginModal.value.hide() loading.value = false
props.nextPage()
const settings = await get().catch(handleError) } catch (err) {
settings.default_user = loggedIn.id loading.value = false
await set(settings).catch(handleError) handleSevereError(err)
finalizedLogin.value = true }
await mixpanel.track('AccountLogIn')
props.nextPage()
}
const openUrl = async () => {
await window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: loginUrl.value,
},
})
}
const clipboardWrite = async (a) => {
navigator.clipboard.writeText(a)
} }
</script> </script>
@ -87,45 +59,15 @@ const clipboardWrite = async (a) => {
<div class="action-row"> <div class="action-row">
<Button class="transparent" large @click="prevPage"> Back </Button> <Button class="transparent" large @click="prevPage"> Back </Button>
<div class="sign-in-pair"> <div class="sign-in-pair">
<Button color="primary" large @click="login"> <Button color="primary" large :disabled="loading" @click="login">
<LogInIcon v-if="!finalizedLogin" /> <LogInIcon />
{{ finalizedLogin ? 'Next' : 'Sign in' }} {{ loading ? 'Loading...' : 'Sign in' }}
</Button> </Button>
</div> </div>
<Button class="transparent" large @click="nextPage()"> Next </Button> <Button class="transparent" large @click="nextPage()"> Finish</Button>
</div> </div>
</Card> </Card>
</div> </div>
<Modal ref="loginModal" header="Signing in" :noblur="!themeStore.advancedRendering">
<div class="modal-body">
<QrcodeVue :value="loginUrl" class="qr-code" margin="3" size="160" />
<div class="modal-text">
<div class="label">Copy this code</div>
<div class="code-text">
<div class="code">
{{ loginCode }}
</div>
<Button
v-tooltip="'Copy code'"
icon-only
large
color="raised"
@click="() => clipboardWrite(loginCode)"
>
<ClipboardCopyIcon />
</Button>
</div>
<div>And enter it on Microsoft's website to sign in.</div>
<div class="iconified-input">
<LogInIcon />
<input type="text" :value="loginUrl" readonly />
<Button v-tooltip="'Open link'" icon-only color="raised" @click="openUrl">
<GlobeIcon />
</Button>
</div>
</div>
</div>
</Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@ -188,79 +130,10 @@ const clipboardWrite = async (a) => {
} }
} }
.qr-code {
background-color: white !important;
border-radius: var(--radius-md);
}
.modal-body {
display: flex;
flex-direction: row;
gap: var(--gap-lg);
align-items: center;
padding: var(--gap-lg);
.modal-text {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
width: 100%;
h2,
p {
margin: 0;
}
}
}
.code-text {
display: flex;
flex-direction: row;
gap: var(--gap-xs);
align-items: center;
.code {
background-color: var(--color-bg);
border-radius: var(--radius-md);
border: solid 1px var(--color-button-bg);
font-family: var(--mono-font);
letter-spacing: var(--gap-md);
color: var(--color-contrast);
font-size: 2rem;
font-weight: bold;
padding: var(--gap-sm) 0 var(--gap-sm) var(--gap-md);
}
.btn {
width: 2.5rem;
height: 2.5rem;
}
}
.sticker {
width: 100%;
max-width: 25rem;
height: auto;
margin-bottom: var(--gap-lg);
}
.sign-in-pair { .sign-in-pair {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap-sm); gap: var(--gap-sm);
align-items: center; align-items: center;
} }
.code {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
.card {
background: var(--color-base);
color: var(--color-contrast);
padding: 0.5rem 1rem;
margin-top: 0.5rem;
}
}
</style> </style>

View File

@ -1,28 +1,6 @@
<script setup> <script setup>
import { import { Button } from 'omorphia'
Button,
HomeIcon,
SearchIcon,
LibraryIcon,
PlusIcon,
SettingsIcon,
XIcon,
Notifications,
} from 'omorphia'
import { appWindow } from '@tauri-apps/api/window'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import FakeAppBar from '@/components/ui/tutorial/FakeAppBar.vue'
import FakeAccountsCard from '@/components/ui/tutorial/FakeAccountsCard.vue'
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator.js'
import FakeSearch from '@/components/ui/tutorial/FakeSearch.vue'
import FakeGridDisplay from '@/components/ui/tutorial/FakeGridDisplay.vue'
import FakeRowDisplay from '@/components/ui/tutorial/FakeRowDisplay.vue'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { window } from '@tauri-apps/api'
import TutorialTip from '@/components/ui/tutorial/TutorialTip.vue'
import FakeSettings from '@/components/ui/tutorial/FakeSettings.vue'
import { get, set } from '@/helpers/settings.js' import { get, set } from '@/helpers/settings.js'
import mixpanel from 'mixpanel-browser' import mixpanel from 'mixpanel-browser'
import GalleryImage from '@/components/ui/tutorial/GalleryImage.vue' import GalleryImage from '@/components/ui/tutorial/GalleryImage.vue'
@ -30,11 +8,7 @@ import LoginCard from '@/components/ui/tutorial/LoginCard.vue'
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue' import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
import { auto_install_java, get_jre } from '@/helpers/jre.js' import { auto_install_java, get_jre } from '@/helpers/jre.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import ImportingCard from '@/components/ui/tutorial/ImportingCard.vue'
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
import PreImportScreen from '@/components/ui/tutorial/PreImportScreen.vue'
const phase = ref(0)
const page = ref(1) const page = ref(1)
const props = defineProps({ const props = defineProps({
@ -46,15 +20,6 @@ const props = defineProps({
const flow = ref('') const flow = ref('')
const nextPhase = () => {
phase.value++
mixpanel.track('TutorialPhase', { page: phase.value })
}
const prevPhase = () => {
phase.value--
}
const nextPage = (newFlow) => { const nextPage = (newFlow) => {
page.value++ page.value++
mixpanel.track('OnboardingPage', { page: page.value }) mixpanel.track('OnboardingPage', { page: page.value })
@ -64,10 +29,6 @@ const nextPage = (newFlow) => {
} }
} }
const endOnboarding = () => {
nextPhase()
}
const prevPage = () => { const prevPage = () => {
page.value-- page.value--
} }
@ -105,18 +66,18 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div v-if="phase === 0" class="onboarding"> <div class="onboarding">
<StickyTitleBar /> <StickyTitleBar />
<GalleryImage <GalleryImage
v-if="page === 1" v-if="page === 1"
:gallery="[ :gallery="[
{ {
url: 'https://cdn.discordapp.com/attachments/817413688771608587/1131109353928265809/Screenshot_2023-07-15_at_4.16.18_PM.png', url: 'https://launcher-files.modrinth.com/onboarding/home.png',
title: 'Discovery', title: 'Discovery',
subtitle: 'See the latest and greatest mods and modpacks to play with from Modrinth', subtitle: 'See the latest and greatest mods and modpacks to play with from Modrinth',
}, },
{ {
url: 'https://cdn.discordapp.com/attachments/817413688771608587/1131109354238640238/Screenshot_2023-07-15_at_4.17.43_PM.png', url: 'https://launcher-files.modrinth.com/onboarding/discover.png',
title: 'Profile Management', title: 'Profile Management',
subtitle: subtitle:
'Play, manage and search through all the amazing profiles downloaded on your computer at any time, even offline!', 'Play, manage and search through all the amazing profiles downloaded on your computer at any time, even offline!',
@ -126,185 +87,7 @@ onMounted(async () => {
> >
<Button color="primary" @click="nextPage"> Get started </Button> <Button color="primary" @click="nextPage"> Get started </Button>
</GalleryImage> </GalleryImage>
<LoginCard v-else-if="page === 2" :next-page="nextPage" :prev-page="prevPage" /> <LoginCard v-else-if="page === 2" :next-page="finishOnboarding" :prev-page="prevPage" />
<ModrinthLoginScreen
v-else-if="page === 3"
:modal="false"
:next-page="nextPage"
:prev-page="prevPage"
:flow="flow"
/>
<PreImportScreen
v-else-if="page === 4"
:next-page="endOnboarding"
:prev-page="prevPage"
:import-page="nextPage"
/>
<ImportingCard v-else-if="page === 5" :next-page="endOnboarding" :prev-page="prevPage" />
</div>
<div v-else class="container">
<StickyTitleBar v-if="phase === 9" />
<div v-if="phase < 9" class="nav-container">
<div class="nav-section">
<FakeAccountsCard :show-demo="phase === 3">
<TutorialTip
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Signing in"
description="The Modrinth App uses your Microsoft account to allow you to launch Minecraft. You can sign in with your Microsoft account here, and switch between multiple accounts."
/>
</FakeAccountsCard>
<div class="pages-list">
<div class="btn icon-only" :class="{ active: phase < 4 }">
<HomeIcon />
</div>
<div
class="btn icon-only"
:class="{ active: phase === 4 || phase === 5, highlighted: phase === 4 }"
>
<SearchIcon />
</div>
<div
class="btn icon-only"
:class="{
active: phase === 6 || phase === 7,
highlighted: phase === 6,
}"
>
<LibraryIcon />
</div>
</div>
</div>
<div class="settings pages-list">
<Button class="sleek-primary" icon-only>
<PlusIcon />
</Button>
<Button icon-only :class="{ active: phase === 8, highlighted: phase === 8 }">
<SettingsIcon />
</Button>
</div>
</div>
<div v-if="phase < 9" class="view">
<div data-tauri-drag-region class="appbar">
<section class="navigation-controls">
<Breadcrumbs data-tauri-drag-region />
</section>
<section class="mod-stats">
<FakeAppBar
:show-running="phase === 7"
:show-download="phase === 5"
:exit="finishOnboarding"
>
<template #running>
<TutorialTip
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Playing modpacks"
description="When you launch a modpack, you can manage it directly in the title bar here. You can stop the modpack, view the logs, and see all currently running packs."
/>
</template>
<template #download>
<TutorialTip
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Installing modpacks"
description="When you download a modpack, Modrinth App will automatically install it for you. You can view the progress of the installation here."
/>
</template>
</FakeAppBar>
</section>
<section class="window-controls">
<Button class="titlebar-button" icon-only @click="() => appWindow.minimize()">
<MinimizeIcon />
</Button>
<Button class="titlebar-button" icon-only @click="() => appWindow.toggleMaximize()">
<MaximizeIcon />
</Button>
<Button
class="titlebar-button close"
icon-only
@click="
() => {
saveWindowState(StateFlags.ALL)
window.getCurrent().close()
}
"
>
<XIcon />
</Button>
</section>
</div>
<div class="router-view">
<ModrinthLoadingIndicator
offset-height="var(--appbar-height)"
offset-width="var(--sidebar-width)"
/>
<Notifications ref="notificationsWrapper" />
<FakeRowDisplay v-if="phase < 4 || phase > 8" :show-instance="phase === 2" />
<FakeGridDisplay v-if="phase === 6 || phase === 7" :show-instances="phase === 6" />
<suspense>
<FakeSearch v-if="phase === 4 || phase === 5" :show-search="phase === 4" />
</suspense>
<FakeSettings v-if="phase === 8" />
</div>
</div>
<TutorialTip
v-if="phase === 1"
class="first-tip highlighted"
:progress-function="nextPhase"
:progress="phase"
title="Enter the Modrinth App!"
description="This is the Modrinth App guide. Key parts are marked with a green shadow. Click 'Next' to
proceed. You can leave the tutorial anytime using the Exit button above the plus button on the bottom left."
/>
<div v-if="phase === 1" class="whole-page-shadow" />
<TutorialTip
v-if="phase === 2"
class="sticky-tip"
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Home page"
description="This is the home page. Here you can see all the latest modpacks, mods, and other content on Modrinth. You can also see a few of your installed modpacks here."
/>
<TutorialTip
v-if="phase === 4"
class="sticky-tip"
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Searching for content"
description="You can search for content on Modrinth by navigating to the search page. You can search for mods, modpacks, and more, and install them directly from here."
/>
<TutorialTip
v-if="phase === 6"
class="sticky-tip"
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Modpack library"
description="You can view all your installed modpacks in the library. You can launch any modpack from here, or click the card to view more information about it."
/>
<TutorialTip
v-if="phase === 8"
class="sticky-tip"
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Settings"
description="You will be able to view and change the settings for the Modrinth App here. You can change the appearance, set and download new Java versions, and more."
/>
<TutorialTip
v-if="phase === 9"
class="final-tip highlighted"
:progress-function="finishOnboarding"
:progress="phase"
title="Enter the Modrinth App!"
description="That's it! You're ready to use the Modrinth App. If you need help, you can always join our discord server!"
/>
</div> </div>
</template> </template>

View File

@ -1,184 +0,0 @@
<script setup>
import { Button, Card, ModrinthIcon } from 'omorphia'
import { ATLauncherIcon, PrismIcon } from '@/assets/external/index.js'
defineProps({
nextPage: {
type: Function,
required: true,
},
prevPage: {
type: Function,
required: true,
},
importPage: {
type: Function,
required: true,
},
})
</script>
<template>
<Card class="import-card">
<div class="base-ellipsis ellipsis-1" />
<div class="base-ellipsis ellipsis-2" />
<div class="base-ellipsis ellipsis-3" />
<div class="base-ellipsis ellipsis-4" />
<div class="logo">
<ModrinthIcon />
</div>
<div class="launcher-stamp top-left">
<ATLauncherIcon />
</div>
<div class="launcher-stamp top-right">
<PrismIcon />
</div>
<div class="launcher-stamp bottom-left">
<img src="@/assets/external/gdlauncher.png" alt="GDLauncher" />
</div>
<div class="launcher-stamp bottom-right">
<img src="@/assets/external/multimc.webp" alt="MultiMC" />
</div>
<div class="info-section">
<h2>Importing</h2>
<div class="markdown-body">
<p>
You can import projects from other launchers by clicking below, or you can skip ahead.
</p>
</div>
<div class="button-row">
<Button class="transparent" @click="prevPage"> Back </Button>
<Button color="primary" @click="importPage"> Import </Button>
<Button class="transparent" @click="nextPage"> Next </Button>
</div>
</div>
</Card>
</template>
<style scoped lang="scss">
.import-card {
width: 40rem;
height: 32rem;
position: relative;
overflow: hidden;
padding: 0;
}
.base-ellipsis {
position: absolute;
left: 50%;
border-radius: 100%;
top: calc(var(--gap-xl) + 5rem);
transform: translate(-50%, -50%);
width: 100%;
background-color: rgba(#1bd96a, 0.1);
}
.ellipsis-1 {
width: 15rem;
height: 15rem;
}
.ellipsis-2 {
width: 30rem;
height: 30rem;
}
.ellipsis-3 {
width: 45rem;
height: 45rem;
}
.logo {
position: absolute;
top: calc(var(--gap-xl) + 5rem);
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
background-color: var(--color-accent-contrast);
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
z-index: 1;
width: 7rem;
height: 7rem;
svg {
width: 100%;
height: 100%;
}
}
.launcher-stamp {
position: absolute;
width: 5rem;
height: 5rem;
background-color: var(--color-accent-contrast);
border-radius: 50%;
z-index: 1;
opacity: 0.65;
padding: var(--gap-lg);
&.top-left {
top: var(--gap-xl);
left: 3rem;
}
&.top-right {
top: var(--gap-xl);
right: 3rem;
}
&.bottom-left {
top: 12rem;
left: 5.5rem;
}
&.bottom-right {
top: 12rem;
right: 5.5rem;
}
svg,
img {
width: 100%;
height: 100%;
}
}
.info-section {
position: absolute;
bottom: var(--gap-xl);
left: 50%;
width: 30rem;
transform: translateX(-50%);
padding: var(--gap-xl);
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
align-items: center;
gap: var(--gap-md);
backdrop-filter: blur(1rem) brightness(0.4);
-webkit-backdrop-filter: blur(1rem) brightness(0.4);
border-radius: var(--radius-lg);
h2 {
margin: 0;
}
}
.button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--gap-md);
width: 100%;
align-content: center;
.transparent {
padding: var(--gap-sm) 0;
}
}
</style>

View File

@ -1,72 +0,0 @@
<script setup>
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { Button, Card } from 'omorphia'
defineProps({
progress: {
type: Number,
default: 0,
},
title: {
type: String,
default: 'Tutorial',
},
description: {
type: String,
default: 'This is a tutorial',
},
progressFunction: {
type: Function,
default: () => {},
},
previousFunction: {
type: Function,
required: false,
default: null,
},
})
</script>
<template>
<Card class="tutorial-card">
<h3 class="tutorial-title">
{{ title }}
</h3>
<div class="tutorial-body">
{{ description }}
</div>
<div class="tutorial-footer">
<Button v-if="previousFunction" class="transparent" @click="previousFunction"> Back </Button>
{{ progress }}/9
<ProgressBar :progress="(progress / 9) * 100" />
<Button color="primary" :action="progressFunction">
{{ progress === 9 ? 'Finish' : 'Next' }}
</Button>
</div>
</Card>
</template>
<style scoped lang="scss">
.tutorial-card {
display: flex;
flex-direction: column;
gap: var(--gap-md);
border: 1px solid var(--color-button-bg);
width: 22rem;
}
.tutorial-title {
margin: 0;
}
.tutorial-footer {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
.transparent {
border: 1px solid var(--color-button-bg);
}
}
</style>

View File

@ -18,28 +18,20 @@ import { invoke } from '@tauri-apps/api/tauri'
/// This returns a DeviceLoginSuccess object, with two relevant fields: /// This returns a DeviceLoginSuccess object, with two relevant fields:
/// - verification_uri: the URL to go to to complete the flow /// - verification_uri: the URL to go to to complete the flow
/// - user_code: the code to enter on the verification_uri page /// - user_code: the code to enter on the verification_uri page
export async function authenticate_begin_flow() { export async function login() {
return await invoke('plugin:auth|auth_authenticate_begin_flow') return await invoke('auth_login')
} }
/// Authenticate a user with Hydra - part 2 /// Retrieves the default user
/// This completes the authentication flow quasi-synchronously, returning the sign-in credentials
/// (and also adding the credentials to the state)
/// This returns a Credentials object
export async function authenticate_await_completion() {
return await invoke('plugin:auth|auth_authenticate_await_completion')
}
export async function cancel_flow() {
return await invoke('plugin:auth|auth_cancel_flow')
}
/// Refresh some credentials using Hydra, if needed
/// user is UUID /// user is UUID
/// update_name is bool export async function get_default_user() {
/// Returns a Credentials object return await invoke('plugin:auth|auth_get_default_user')
export async function refresh(user, update_name) { }
return await invoke('plugin:auth|auth_refresh', { user, update_name })
/// Updates the default user
/// user is UUID
export async function set_default_user(user) {
return await invoke('plugin:auth|auth_set_default_user', { user })
} }
/// Remove a user account from the database /// Remove a user account from the database
@ -48,13 +40,6 @@ export async function remove_user(user) {
return await invoke('plugin:auth|auth_remove_user', { user }) return await invoke('plugin:auth|auth_remove_user', { user })
} }
// Add a path as a profile in-memory
// user is UUID
/// Returns a bool
export async function has_user(user) {
return await invoke('plugin:auth|auth_has_user', { user })
}
/// Returns a list of users /// Returns a list of users
/// Returns an Array of Credentials /// Returns an Array of Credentials
export async function users() { export async function users() {

View File

@ -0,0 +1,21 @@
import { defineStore } from 'pinia'
export const useError = defineStore('errorsStore', {
state: () => ({
errorModal: null,
}),
actions: {
setErrorModal(ref) {
this.errorModal = ref
},
showError(error) {
this.errorModal.show(error)
},
},
})
export const handleSevereError = (err) => {
const error = useError()
error.showError(err)
console.error(err)
}

View File

@ -14,15 +14,20 @@ use theseus::profile::create::profile_create;
// 3) call the authenticate_await_complete_flow() function to get the credentials (like you would in the frontend) // 3) call the authenticate_await_complete_flow() function to get the credentials (like you would in the frontend)
pub async fn authenticate_run() -> theseus::Result<Credentials> { pub async fn authenticate_run() -> theseus::Result<Credentials> {
println!("A browser window will now open, follow the login flow there."); println!("A browser window will now open, follow the login flow there.");
let login = auth::authenticate_begin_flow().await?; let login = minecraft_auth::begin_login().await?;
println!("URL {}", login.verification_uri.as_str()); println!("URL {}", login.redirect_uri.as_str());
println!("Code {}", login.user_code.as_str()); webbrowser::open(login.redirect_uri.as_str())?;
webbrowser::open(login.verification_uri.as_str())
.map_err(|e| IOError::with_path(e, login.verification_uri.as_str()))?;
let credentials = auth::authenticate_await_complete_flow().await?; println!("Please enter URL code: ");
State::sync().await?; let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("error: unable to read user input");
println!("You entered: {}", input.trim());
let credentials = minecraft_auth::finish_login(&input, login).await?;
println!("Logged in user {}.", credentials.username); println!("Logged in user {}.", credentials.username);
Ok(credentials) Ok(credentials)
@ -38,6 +43,11 @@ async fn main() -> theseus::Result<()> {
let st = State::get().await?; let st = State::get().await?;
//State::update(); //State::update();
if minecraft_auth::users().await?.is_empty() {
println!("No users found, authenticating.");
authenticate_run().await?; // could take credentials from here direct, but also deposited in state users
}
// Autodetect java globals // Autodetect java globals
let jres = jre::get_all_jre().await?; let jres = jre::get_all_jre().await?;
let java_8 = jre::find_filtered_jres("1.8", jres.clone(), false).await?; let java_8 = jre::find_filtered_jres("1.8", jres.clone(), false).await?;
@ -91,12 +101,6 @@ async fn main() -> theseus::Result<()> {
State::sync().await?; State::sync().await?;
// Attempt to run game
if auth::users().await?.is_empty() {
println!("No users found, authenticating.");
authenticate_run().await?; // could take credentials from here direct, but also deposited in state users
}
println!("running"); println!("running");
// Run a profile, running minecraft and store the RwLock to the process // Run a profile, running minecraft and store the RwLock to the process
let proc_lock = profile::run(&profile_path).await?; let proc_lock = profile::run(&profile_path).await?;