feat: habilitar provisionamento desktop e rotas CORS
This commit is contained in:
parent
7569986ffc
commit
152550a9a0
19 changed files with 1806 additions and 211 deletions
|
|
@ -1,19 +1,23 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="pt-BR">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="stylesheet" href="/src/styles.css" />
|
<link rel="stylesheet" href="/src/styles.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Sistema de Chamados Desktop</title>
|
<title>Sistema de Chamados — Agente Desktop</title>
|
||||||
<script type="module" src="/src/main.ts" defer></script>
|
<script type="module" src="/src/main.ts" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main style="height: 100vh; display: grid; place-items: center;">
|
<main id="app-root" class="app-root">
|
||||||
<div style="text-align: center; font-family: system-ui, sans-serif;">
|
<section class="card">
|
||||||
<h1 style="margin-bottom: 0.5rem;">Abrindo Sistema de Chamados…</h1>
|
<header>
|
||||||
<p style="color: #555;">Certifique-se de que o serviço web está disponível em <code>VITE_APP_URL</code>.</p>
|
<h1>Sistema de Chamados</h1>
|
||||||
</div>
|
<p class="subtitle">Agente desktop para provisionamento de máquinas</p>
|
||||||
|
</header>
|
||||||
|
<div id="alert-container" class="alert"></div>
|
||||||
|
<div id="content"></div>
|
||||||
|
<p id="status-text" class="status-text"></p>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2"
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-store": "^2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
|
|
||||||
442
apps/desktop/src-tauri/Cargo.lock
generated
442
apps/desktop/src-tauri/Cargo.lock
generated
|
|
@ -60,11 +60,20 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
name = "appsdesktop"
|
name = "appsdesktop"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"hostname",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sysinfo",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-store",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -486,8 +495,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -593,6 +604,25 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
|
|
@ -821,6 +851,12 @@ version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embed-resource"
|
name = "embed-resource"
|
||||||
version = "3.0.6"
|
version = "3.0.6"
|
||||||
|
|
@ -1230,8 +1266,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1241,9 +1279,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasi 0.14.7+wasi-0.2.4",
|
"wasi 0.14.7+wasi-0.2.4",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1436,6 +1476,17 @@ version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.29.1"
|
version = "0.29.1"
|
||||||
|
|
@ -1509,6 +1560,23 @@ dependencies = [
|
||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.17"
|
version = "0.1.17"
|
||||||
|
|
@ -1947,6 +2015,12 @@ version = "0.4.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-slab"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -2102,6 +2176,15 @@ version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntapi"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -2822,6 +2905,61 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"lru-slab",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.41"
|
version = "1.0.41"
|
||||||
|
|
@ -2862,6 +3000,16 @@ dependencies = [
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -2882,6 +3030,16 @@ dependencies = [
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
@ -2900,6 +3058,15 @@ dependencies = [
|
||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_hc"
|
name = "rand_hc"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -2924,6 +3091,26 @@ version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
|
@ -3007,16 +3194,21 @@ dependencies = [
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
|
@ -3026,6 +3218,21 @@ dependencies = [
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3034,6 +3241,12 @@ version = "0.1.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -3056,6 +3269,41 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||||
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|
@ -3489,6 +3737,12 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
|
@ -3542,6 +3796,20 @@ dependencies = [
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sysinfo"
|
||||||
|
version = "0.31.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"memchr",
|
||||||
|
"ntapi",
|
||||||
|
"rayon",
|
||||||
|
"windows 0.57.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "6.2.2"
|
version = "6.2.2"
|
||||||
|
|
@ -3589,7 +3857,7 @@ dependencies = [
|
||||||
"tao-macros",
|
"tao-macros",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
|
|
@ -3661,7 +3929,7 @@ dependencies = [
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3762,10 +4030,26 @@ dependencies = [
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-store"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
|
|
@ -3788,7 +4072,7 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3814,7 +4098,7 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3971,6 +4255,21 @@ dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec_macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.47.1"
|
version = "1.47.1"
|
||||||
|
|
@ -3985,9 +4284,31 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.16"
|
version = "0.7.16"
|
||||||
|
|
@ -4277,6 +4598,12 @@ version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.7"
|
version = "2.5.7"
|
||||||
|
|
@ -4501,6 +4828,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webkit2gtk"
|
name = "webkit2gtk"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
|
@ -4545,6 +4882,15 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
|
|
@ -4553,10 +4899,10 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webview2-com-macros",
|
"webview2-com-macros",
|
||||||
"webview2-com-sys",
|
"webview2-com-sys",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-implement",
|
"windows-implement 0.60.2",
|
||||||
"windows-interface",
|
"windows-interface 0.59.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4577,7 +4923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
|
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4627,6 +4973,16 @@ dependencies = [
|
||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.57.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.57.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.61.3"
|
version = "0.61.3"
|
||||||
|
|
@ -4649,14 +5005,26 @@ dependencies = [
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.57.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement 0.57.0",
|
||||||
|
"windows-interface 0.57.0",
|
||||||
|
"windows-result 0.1.2",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement 0.60.2",
|
||||||
"windows-interface",
|
"windows-interface 0.59.3",
|
||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
"windows-result 0.3.4",
|
"windows-result 0.3.4",
|
||||||
"windows-strings 0.4.2",
|
"windows-strings 0.4.2",
|
||||||
|
|
@ -4668,8 +5036,8 @@ version = "0.62.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement 0.60.2",
|
||||||
"windows-interface",
|
"windows-interface 0.59.3",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
"windows-result 0.4.1",
|
"windows-result 0.4.1",
|
||||||
"windows-strings 0.5.1",
|
"windows-strings 0.5.1",
|
||||||
|
|
@ -4686,6 +5054,17 @@ dependencies = [
|
||||||
"windows-threading",
|
"windows-threading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.57.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.60.2"
|
version = "0.60.2"
|
||||||
|
|
@ -4697,6 +5076,17 @@ dependencies = [
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.57.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.59.3"
|
version = "0.59.3"
|
||||||
|
|
@ -4730,6 +5120,15 @@ dependencies = [
|
||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
|
@ -4775,6 +5174,15 @@ dependencies = [
|
||||||
"windows-targets 0.42.2",
|
"windows-targets 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
|
|
@ -5085,7 +5493,7 @@ dependencies = [
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
|
|
@ -5237,6 +5645,12 @@ dependencies = [
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,16 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = ["wry"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-store = "2.4"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system"] }
|
||||||
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
||||||
|
once_cell = "1.19"
|
||||||
|
thiserror = "1.0"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
parking_lot = "0.12"
|
||||||
|
hostname = "0.4"
|
||||||
|
|
|
||||||
333
apps/desktop/src-tauri/src/agent.rs
Normal file
333
apps/desktop/src-tauri/src/agent.rs
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use serde::Serialize;
|
||||||
|
use sysinfo::{Networks, System};
|
||||||
|
use tauri::async_runtime::{self, JoinHandle};
|
||||||
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum AgentError {
|
||||||
|
#[error("Falha ao obter hostname da máquina")]
|
||||||
|
Hostname,
|
||||||
|
#[error("Nenhum identificador de hardware disponível (MAC/serial)")]
|
||||||
|
MissingIdentifiers,
|
||||||
|
#[error("URL de API inválida")]
|
||||||
|
InvalidApiUrl,
|
||||||
|
#[error("Falha HTTP: {0}")]
|
||||||
|
Http(#[from] reqwest::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MachineOs {
|
||||||
|
pub name: String,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub architecture: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MachineMetrics {
|
||||||
|
pub collected_at: DateTime<Utc>,
|
||||||
|
pub cpu_logical_cores: usize,
|
||||||
|
pub cpu_physical_cores: Option<usize>,
|
||||||
|
pub cpu_usage_percent: f32,
|
||||||
|
pub load_average_one: Option<f64>,
|
||||||
|
pub load_average_five: Option<f64>,
|
||||||
|
pub load_average_fifteen: Option<f64>,
|
||||||
|
pub memory_total_bytes: u64,
|
||||||
|
pub memory_used_bytes: u64,
|
||||||
|
pub memory_used_percent: f32,
|
||||||
|
pub uptime_seconds: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MachineInventory {
|
||||||
|
pub cpu_brand: Option<String>,
|
||||||
|
pub host_identifier: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MachineProfile {
|
||||||
|
pub hostname: String,
|
||||||
|
pub os: MachineOs,
|
||||||
|
pub mac_addresses: Vec<String>,
|
||||||
|
pub serial_numbers: Vec<String>,
|
||||||
|
pub inventory: MachineInventory,
|
||||||
|
pub metrics: MachineMetrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct HeartbeatPayload {
|
||||||
|
machine_token: String,
|
||||||
|
status: Option<String>,
|
||||||
|
hostname: Option<String>,
|
||||||
|
os: Option<MachineOs>,
|
||||||
|
metrics: Option<MachineMetrics>,
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_mac_addresses() -> Vec<String> {
|
||||||
|
let mut macs = Vec::new();
|
||||||
|
let mut networks = Networks::new();
|
||||||
|
networks.refresh_list();
|
||||||
|
networks.refresh();
|
||||||
|
|
||||||
|
for (_, data) in networks.iter() {
|
||||||
|
let bytes = data.mac_address().0;
|
||||||
|
if bytes.iter().all(|byte| *byte == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = bytes
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{:02x}", byte))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(":");
|
||||||
|
|
||||||
|
if !macs.contains(&formatted) {
|
||||||
|
macs.push(formatted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_system() -> System {
|
||||||
|
let mut system = System::new_all();
|
||||||
|
system.refresh_all();
|
||||||
|
system
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_metrics(system: &System) -> MachineMetrics {
|
||||||
|
let collected_at = Utc::now();
|
||||||
|
let total_memory = system.total_memory();
|
||||||
|
let used_memory = system.used_memory();
|
||||||
|
let memory_total_bytes = total_memory.saturating_mul(1024);
|
||||||
|
let memory_used_bytes = used_memory.saturating_mul(1024);
|
||||||
|
let memory_used_percent = if total_memory > 0 {
|
||||||
|
(used_memory as f32 / total_memory as f32) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let load = System::load_average();
|
||||||
|
let cpu_usage_percent = system.global_cpu_usage();
|
||||||
|
let cpu_logical_cores = system.cpus().len();
|
||||||
|
let cpu_physical_cores = system.physical_core_count();
|
||||||
|
|
||||||
|
MachineMetrics {
|
||||||
|
collected_at,
|
||||||
|
cpu_logical_cores,
|
||||||
|
cpu_physical_cores,
|
||||||
|
cpu_usage_percent,
|
||||||
|
load_average_one: Some(load.one),
|
||||||
|
load_average_five: Some(load.five),
|
||||||
|
load_average_fifteen: Some(load.fifteen),
|
||||||
|
memory_total_bytes,
|
||||||
|
memory_used_bytes,
|
||||||
|
memory_used_percent,
|
||||||
|
uptime_seconds: System::uptime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_profile() -> Result<MachineProfile, AgentError> {
|
||||||
|
let hostname = hostname::get()
|
||||||
|
.map_err(|_| AgentError::Hostname)?
|
||||||
|
.to_string_lossy()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let system = collect_system();
|
||||||
|
|
||||||
|
let os_name = System::name()
|
||||||
|
.or_else(|| System::long_os_version())
|
||||||
|
.unwrap_or_else(|| "desconhecido".to_string());
|
||||||
|
let os_version = System::os_version();
|
||||||
|
let architecture = std::env::consts::ARCH.to_string();
|
||||||
|
|
||||||
|
let mac_addresses = collect_mac_addresses();
|
||||||
|
let serials: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if mac_addresses.is_empty() && serials.is_empty() {
|
||||||
|
return Err(AgentError::MissingIdentifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
let metrics = collect_metrics(&system);
|
||||||
|
let cpu_brand = system
|
||||||
|
.cpus()
|
||||||
|
.first()
|
||||||
|
.map(|cpu| cpu.brand().to_string())
|
||||||
|
.filter(|brand| !brand.trim().is_empty());
|
||||||
|
|
||||||
|
let inventory = MachineInventory {
|
||||||
|
cpu_brand,
|
||||||
|
host_identifier: serials.first().cloned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(MachineProfile {
|
||||||
|
hostname,
|
||||||
|
os: MachineOs {
|
||||||
|
name: os_name,
|
||||||
|
version: os_version,
|
||||||
|
architecture: Some(architecture),
|
||||||
|
},
|
||||||
|
mac_addresses,
|
||||||
|
serial_numbers: serials,
|
||||||
|
inventory,
|
||||||
|
metrics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static HTTP_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.user_agent("sistema-de-chamados-agent/1.0")
|
||||||
|
.timeout(Duration::from_secs(20))
|
||||||
|
.use_rustls_tls()
|
||||||
|
.build()
|
||||||
|
.expect("failed to build http client")
|
||||||
|
});
|
||||||
|
|
||||||
|
async fn post_heartbeat(base_url: &str, token: &str, status: Option<String>) -> Result<(), AgentError> {
|
||||||
|
let system = collect_system();
|
||||||
|
let metrics = collect_metrics(&system);
|
||||||
|
let hostname = hostname::get()
|
||||||
|
.map_err(|_| AgentError::Hostname)?
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
let os = MachineOs {
|
||||||
|
name: System::name()
|
||||||
|
.or_else(|| System::long_os_version())
|
||||||
|
.unwrap_or_else(|| "desconhecido".to_string()),
|
||||||
|
version: System::os_version(),
|
||||||
|
architecture: Some(std::env::consts::ARCH.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload = HeartbeatPayload {
|
||||||
|
machine_token: token.to_string(),
|
||||||
|
status,
|
||||||
|
hostname: Some(hostname),
|
||||||
|
os: Some(os),
|
||||||
|
metrics: Some(metrics),
|
||||||
|
metadata: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/api/machines/heartbeat", base_url);
|
||||||
|
HTTP_CLIENT.post(url).json(&payload).send().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HeartbeatHandle {
|
||||||
|
token: String,
|
||||||
|
base_url: String,
|
||||||
|
status: Option<String>,
|
||||||
|
stop_signal: Arc<Notify>,
|
||||||
|
join_handle: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeartbeatHandle {
|
||||||
|
fn stop(self) {
|
||||||
|
self.stop_signal.notify_waiters();
|
||||||
|
self.join_handle.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AgentRuntime {
|
||||||
|
inner: Mutex<Option<HeartbeatHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_base_url(input: &str) -> Result<String, AgentError> {
|
||||||
|
let trimmed = input.trim().trim_end_matches('/');
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err(AgentError::InvalidApiUrl);
|
||||||
|
}
|
||||||
|
Ok(trimmed.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentRuntime {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_heartbeat(
|
||||||
|
&self,
|
||||||
|
base_url: String,
|
||||||
|
token: String,
|
||||||
|
status: Option<String>,
|
||||||
|
interval_seconds: Option<u64>,
|
||||||
|
) -> Result<(), AgentError> {
|
||||||
|
let sanitized_base = sanitize_base_url(&base_url)?;
|
||||||
|
let interval = interval_seconds.unwrap_or(300).max(60);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut guard = self.inner.lock();
|
||||||
|
if let Some(handle) = guard.take() {
|
||||||
|
if handle.token == token && handle.base_url == sanitized_base {
|
||||||
|
// Reuse existing heartbeat; keep running.
|
||||||
|
*guard = Some(handle);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
handle.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stop_signal = Arc::new(Notify::new());
|
||||||
|
let stop_signal_clone = stop_signal.clone();
|
||||||
|
let token_clone = token.clone();
|
||||||
|
let base_clone = sanitized_base.clone();
|
||||||
|
let status_clone = status.clone();
|
||||||
|
|
||||||
|
let join_handle = async_runtime::spawn(async move {
|
||||||
|
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
|
||||||
|
eprintln!("[agent] Falha inicial ao enviar heartbeat: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ticker = tokio::time::interval(Duration::from_secs(interval));
|
||||||
|
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Wait interval
|
||||||
|
tokio::select! {
|
||||||
|
_ = stop_signal_clone.notified() => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = ticker.tick() => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
|
||||||
|
eprintln!("[agent] Falha ao enviar heartbeat: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let handle = HeartbeatHandle {
|
||||||
|
token,
|
||||||
|
base_url: sanitized_base,
|
||||||
|
status,
|
||||||
|
stop_signal,
|
||||||
|
join_handle,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut guard = self.inner.lock();
|
||||||
|
*guard = Some(handle);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) {
|
||||||
|
let mut guard = self.inner.lock();
|
||||||
|
if let Some(handle) = guard.take() {
|
||||||
|
handle.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,43 @@
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
mod agent;
|
||||||
|
|
||||||
|
use agent::{collect_profile, AgentRuntime, MachineProfile};
|
||||||
|
use tauri_plugin_store::Builder as StorePluginBuilder;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn greet(name: &str) -> String {
|
fn collect_machine_profile() -> Result<MachineProfile, String> {
|
||||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
collect_profile().map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn start_machine_agent(
|
||||||
|
state: tauri::State<AgentRuntime>,
|
||||||
|
base_url: String,
|
||||||
|
token: String,
|
||||||
|
status: Option<String>,
|
||||||
|
interval_seconds: Option<u64>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state
|
||||||
|
.start_heartbeat(base_url, token, status, interval_seconds)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn stop_machine_agent(state: tauri::State<AgentRuntime>) -> Result<(), String> {
|
||||||
|
state.stop();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.manage(AgentRuntime::new())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![greet])
|
.plugin(StorePluginBuilder::default().build())
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
collect_machine_profile,
|
||||||
|
start_machine_agent,
|
||||||
|
stop_machine_agent
|
||||||
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "appsdesktop",
|
"productName": "Sistema de Chamados Desktop",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.renan.appsdesktop",
|
"identifier": "br.com.esdrasrenan.sistemadechamados",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "",
|
"beforeDevCommand": "pnpm run dev",
|
||||||
"devUrl": "http://localhost:3000",
|
"devUrl": "http://localhost:1420",
|
||||||
"beforeBuildCommand": "",
|
"beforeBuildCommand": "pnpm run build",
|
||||||
"frontendDist": ""
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "appsdesktop",
|
"title": "Sistema de Chamados",
|
||||||
"width": 800,
|
"width": 1100,
|
||||||
"height": 600
|
"height": 720,
|
||||||
|
"resizable": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,67 @@
|
||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import { Store } from "@tauri-apps/plugin-store"
|
||||||
|
|
||||||
|
type MachineOs = {
|
||||||
|
name: string
|
||||||
|
version?: string | null
|
||||||
|
architecture?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineMetrics = {
|
||||||
|
collectedAt: string
|
||||||
|
cpuLogicalCores: number
|
||||||
|
cpuPhysicalCores?: number | null
|
||||||
|
cpuUsagePercent: number
|
||||||
|
loadAverageOne?: number | null
|
||||||
|
loadAverageFive?: number | null
|
||||||
|
loadAverageFifteen?: number | null
|
||||||
|
memoryTotalBytes: number
|
||||||
|
memoryUsedBytes: number
|
||||||
|
memoryUsedPercent: number
|
||||||
|
uptimeSeconds: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineInventory = {
|
||||||
|
cpuBrand?: string | null
|
||||||
|
hostIdentifier?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineProfile = {
|
||||||
|
hostname: string
|
||||||
|
os: MachineOs
|
||||||
|
macAddresses: string[]
|
||||||
|
serialNumbers: string[]
|
||||||
|
inventory: MachineInventory
|
||||||
|
metrics: MachineMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineRegisterResponse = {
|
||||||
|
machineId: string
|
||||||
|
tenantId?: string | null
|
||||||
|
companyId?: string | null
|
||||||
|
companySlug?: string | null
|
||||||
|
machineToken: string
|
||||||
|
machineEmail?: string | null
|
||||||
|
expiresAt?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentConfig = {
|
||||||
|
machineId: string
|
||||||
|
machineToken: string
|
||||||
|
tenantId?: string | null
|
||||||
|
companySlug?: string | null
|
||||||
|
machineEmail?: string | null
|
||||||
|
apiBaseUrl: string
|
||||||
|
appUrl: string
|
||||||
|
createdAt: number
|
||||||
|
lastSyncedAt?: number | null
|
||||||
|
expiresAt?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APP_URL?: string
|
readonly VITE_APP_URL?: string
|
||||||
|
readonly VITE_API_BASE_URL?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|
@ -8,23 +69,370 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_URL = "http://localhost:3000";
|
const STORE_FILENAME = "machine-agent.json"
|
||||||
|
const DEFAULT_APP_URL = "http://localhost:3000"
|
||||||
|
|
||||||
function resolveTargetUrl() {
|
function normalizeUrl(value?: string | null, fallback = DEFAULT_APP_URL) {
|
||||||
const fromEnv = import.meta?.env?.VITE_APP_URL;
|
const trimmed = (value ?? fallback).trim()
|
||||||
if (fromEnv && fromEnv.trim().length > 0) {
|
if (!trimmed.startsWith("http")) {
|
||||||
return fromEnv.trim();
|
return fallback
|
||||||
}
|
}
|
||||||
return DEFAULT_URL;
|
return trimmed.replace(/\/+$/, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
function bootstrap() {
|
const appUrl = normalizeUrl(import.meta.env.VITE_APP_URL, DEFAULT_APP_URL)
|
||||||
const targetUrl = resolveTargetUrl();
|
const apiBaseUrl = normalizeUrl(import.meta.env.VITE_API_BASE_URL, appUrl)
|
||||||
if (!targetUrl.startsWith("http")) {
|
|
||||||
console.error("URL inválida para o app desktop:", targetUrl);
|
const alertElement = document.getElementById("alert-container") as HTMLDivElement | null
|
||||||
return;
|
const contentElement = document.getElementById("content") as HTMLDivElement | null
|
||||||
|
const statusElement = document.getElementById("status-text") as HTMLParagraphElement | null
|
||||||
|
|
||||||
|
function setAlert(message: string | null, variant: "info" | "error" | "success" = "info") {
|
||||||
|
if (!alertElement) return
|
||||||
|
if (!message) {
|
||||||
|
alertElement.textContent = ""
|
||||||
|
alertElement.className = "alert"
|
||||||
|
return
|
||||||
}
|
}
|
||||||
window.location.replace(targetUrl);
|
alertElement.textContent = message
|
||||||
|
const extra = variant === "info" ? "" : ` ${variant}`
|
||||||
|
alertElement.className = `alert visible${extra}`
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", bootstrap);
|
function setStatus(message: string) {
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.textContent = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Store(STORE_FILENAME)
|
||||||
|
let storeLoaded = false
|
||||||
|
|
||||||
|
async function ensureStoreLoaded() {
|
||||||
|
if (!storeLoaded) {
|
||||||
|
try {
|
||||||
|
await store.load()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[agent] Falha ao carregar store", error)
|
||||||
|
}
|
||||||
|
storeLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig(): Promise<AgentConfig | null> {
|
||||||
|
try {
|
||||||
|
await ensureStoreLoaded()
|
||||||
|
const record = await store.get<AgentConfig>("config")
|
||||||
|
if (!record) return null
|
||||||
|
return record
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[agent] Falha ao recuperar configuração", error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig(config: AgentConfig) {
|
||||||
|
await ensureStoreLoaded()
|
||||||
|
await store.set("config", config)
|
||||||
|
await store.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearConfig() {
|
||||||
|
await ensureStoreLoaded()
|
||||||
|
await store.delete("config")
|
||||||
|
await store.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectMachineProfile(): Promise<MachineProfile> {
|
||||||
|
return await invoke<MachineProfile>("collect_machine_profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startHeartbeat(config: AgentConfig) {
|
||||||
|
await invoke("start_machine_agent", {
|
||||||
|
baseUrl: config.apiBaseUrl,
|
||||||
|
token: config.machineToken,
|
||||||
|
status: "online",
|
||||||
|
intervalSeconds: 300,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopHeartbeat() {
|
||||||
|
await invoke("stop_machine_agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number) {
|
||||||
|
if (!bytes || Number.isNaN(bytes)) return "—"
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
let value = bytes
|
||||||
|
let index = 0
|
||||||
|
while (value >= 1024 && index < units.length - 1) {
|
||||||
|
value /= 1024
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent(value: number) {
|
||||||
|
if (Number.isNaN(value)) return "—"
|
||||||
|
return `${value.toFixed(1)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(timestamp?: number | null) {
|
||||||
|
if (!timestamp) return "—"
|
||||||
|
try {
|
||||||
|
return new Date(timestamp).toLocaleString()
|
||||||
|
} catch {
|
||||||
|
return "—"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMachineSummary(profile: MachineProfile) {
|
||||||
|
if (!contentElement) return
|
||||||
|
const macs = profile.macAddresses.length > 0 ? profile.macAddresses.join(", ") : "—"
|
||||||
|
const serials = profile.serialNumbers.length > 0 ? profile.serialNumbers.join(", ") : "—"
|
||||||
|
const metrics = profile.metrics
|
||||||
|
const lastCollection = metrics.collectedAt ? new Date(metrics.collectedAt).toLocaleString() : "—"
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="machine-summary">
|
||||||
|
<div><strong>Hostname:</strong> ${profile.hostname}</div>
|
||||||
|
<div><strong>Sistema:</strong> ${profile.os.name}${profile.os.version ? ` ${profile.os.version}` : ""} (${profile.os.architecture ?? "?"})</div>
|
||||||
|
<div><strong>Endereços MAC:</strong> ${macs}</div>
|
||||||
|
<div><strong>Identificadores:</strong> ${serials}</div>
|
||||||
|
<div><strong>CPU:</strong> ${metrics.cpuPhysicalCores ?? metrics.cpuLogicalCores} núcleos · uso ${formatPercent(metrics.cpuUsagePercent)}</div>
|
||||||
|
<div><strong>Memória:</strong> ${formatBytes(metrics.memoryUsedBytes)} / ${formatBytes(metrics.memoryTotalBytes)} (${formatPercent(metrics.memoryUsedPercent)})</div>
|
||||||
|
<div><strong>Coletado em:</strong> ${lastCollection}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRegistered(config: AgentConfig) {
|
||||||
|
if (!contentElement) return
|
||||||
|
const summaryHtml = `
|
||||||
|
<div class="machine-summary">
|
||||||
|
<div><strong>ID da máquina:</strong> ${config.machineId}</div>
|
||||||
|
<div><strong>Email vinculado:</strong> ${config.machineEmail ?? "—"}</div>
|
||||||
|
<div><strong>Tenant:</strong> ${config.tenantId ?? "padrão"}</div>
|
||||||
|
<div><strong>Empresa:</strong> ${config.companySlug ?? "não vinculada"}</div>
|
||||||
|
<div><strong>Token expira em:</strong> ${formatDate(config.expiresAt)}</div>
|
||||||
|
<div><strong>Última sincronização:</strong> ${formatDate(config.lastSyncedAt)}</div>
|
||||||
|
<div><strong>Ambiente:</strong> ${config.appUrl}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
contentElement.innerHTML = `
|
||||||
|
<p>Esta máquina já está provisionada e com heartbeat ativo.</p>
|
||||||
|
${summaryHtml}
|
||||||
|
<div class="actions">
|
||||||
|
<button id="open-app">Abrir sistema</button>
|
||||||
|
<button class="secondary" id="reset-agent">Reprovisionar</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
const openButton = document.getElementById("open-app")
|
||||||
|
const resetButton = document.getElementById("reset-agent")
|
||||||
|
|
||||||
|
openButton?.addEventListener("click", () => redirectToApp(config))
|
||||||
|
resetButton?.addEventListener("click", async () => {
|
||||||
|
await stopHeartbeat().catch(() => undefined)
|
||||||
|
await clearConfig()
|
||||||
|
setAlert("Configuração removida. Reiniciando fluxo de provisionamento.", "success")
|
||||||
|
setTimeout(() => window.location.reload(), 600)
|
||||||
|
})
|
||||||
|
|
||||||
|
setStatus("Máquina provisionada. Redirecionando para a interface web…")
|
||||||
|
setTimeout(() => redirectToApp(config), 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProvisionForm(profile: MachineProfile) {
|
||||||
|
if (!contentElement) return
|
||||||
|
|
||||||
|
const summary = renderMachineSummary(profile) ?? ""
|
||||||
|
|
||||||
|
contentElement.innerHTML = `
|
||||||
|
<form id="provision-form">
|
||||||
|
<label>
|
||||||
|
Código de provisionamento
|
||||||
|
<input type="password" name="provisioningSecret" placeholder="Insira o código fornecido" required autocomplete="one-time-code" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Tenant (opcional)
|
||||||
|
<span class="optional">Use apenas se houver múltiplos ambientes</span>
|
||||||
|
<input type="text" name="tenantId" placeholder="Ex.: tenant-atlas" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Empresa (slug opcional)
|
||||||
|
<span class="optional">Informe para vincular à empresa correta</span>
|
||||||
|
<input type="text" name="companySlug" placeholder="Ex.: empresa-exemplo" />
|
||||||
|
</label>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit">Registrar máquina</button>
|
||||||
|
<button type="button" class="secondary" id="refresh-profile">Atualizar dados</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
${summary}
|
||||||
|
`
|
||||||
|
|
||||||
|
const form = document.getElementById("provision-form") as HTMLFormElement | null
|
||||||
|
const refreshButton = document.getElementById("refresh-profile")
|
||||||
|
|
||||||
|
form?.addEventListener("submit", (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
handleRegister(profile, form)
|
||||||
|
})
|
||||||
|
|
||||||
|
refreshButton?.addEventListener("click", async () => {
|
||||||
|
setStatus("Recolhendo informações atualizadas da máquina…")
|
||||||
|
try {
|
||||||
|
const updatedProfile = await collectMachineProfile()
|
||||||
|
renderProvisionForm(updatedProfile)
|
||||||
|
setStatus("Dados atualizados. Revise e confirme o provisionamento.")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[agent] Falha ao atualizar perfil da máquina", error)
|
||||||
|
setAlert("Não foi possível atualizar as informações da máquina.", "error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRegister(profile: MachineProfile, form: HTMLFormElement) {
|
||||||
|
const submitButton = form.querySelector("button[type=submit]") as HTMLButtonElement | null
|
||||||
|
const formData = new FormData(form)
|
||||||
|
const provisioningSecret = (formData.get("provisioningSecret") as string | null)?.trim()
|
||||||
|
const tenantId = (formData.get("tenantId") as string | null)?.trim()
|
||||||
|
const companySlug = (formData.get("companySlug") as string | null)?.trim()
|
||||||
|
|
||||||
|
if (!provisioningSecret) {
|
||||||
|
setAlert("Informe o código de provisionamento.", "error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = true
|
||||||
|
}
|
||||||
|
setAlert(null)
|
||||||
|
setStatus("Enviando dados de registro da máquina…")
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
provisioningSecret,
|
||||||
|
tenantId: tenantId && tenantId.length > 0 ? tenantId : undefined,
|
||||||
|
companySlug: companySlug && companySlug.length > 0 ? companySlug : undefined,
|
||||||
|
hostname: profile.hostname,
|
||||||
|
os: profile.os,
|
||||||
|
macAddresses: profile.macAddresses,
|
||||||
|
serialNumbers: profile.serialNumbers,
|
||||||
|
metadata: {
|
||||||
|
inventory: profile.inventory,
|
||||||
|
metrics: profile.metrics,
|
||||||
|
},
|
||||||
|
registeredBy: "desktop-agent",
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${apiBaseUrl}/api/machines/register`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let message = `Falha ao registrar máquina (${response.status})`
|
||||||
|
try {
|
||||||
|
const errorBody = await response.json()
|
||||||
|
if (errorBody?.error) {
|
||||||
|
message = errorBody.error
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as MachineRegisterResponse
|
||||||
|
const config: AgentConfig = {
|
||||||
|
machineId: data.machineId,
|
||||||
|
machineToken: data.machineToken,
|
||||||
|
tenantId: data.tenantId ?? null,
|
||||||
|
companySlug: data.companySlug ?? null,
|
||||||
|
machineEmail: data.machineEmail ?? null,
|
||||||
|
apiBaseUrl,
|
||||||
|
appUrl,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
lastSyncedAt: Date.now(),
|
||||||
|
expiresAt: data.expiresAt ?? null,
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveConfig(config)
|
||||||
|
await startHeartbeat(config)
|
||||||
|
|
||||||
|
setAlert("Máquina registrada com sucesso! Abrindo a interface web…", "success")
|
||||||
|
setStatus("Autenticando dispositivo e abrindo o Sistema de Chamados.")
|
||||||
|
|
||||||
|
setTimeout(() => redirectToApp(config), 800)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[agent] Erro no registro da máquina", error)
|
||||||
|
const message = error instanceof Error ? error.message : "Erro desconhecido ao registrar a máquina."
|
||||||
|
const normalized = message.toLowerCase()
|
||||||
|
if (normalized.includes("failed to fetch") || normalized.includes("load failed") || normalized.includes("network")) {
|
||||||
|
setAlert("Não foi possível se conectar ao servidor. Verifique a conexão e o endereço configurado.", "error")
|
||||||
|
} else if (normalized.includes("401") || normalized.includes("403") || normalized.includes("código de provisionamento inválido")) {
|
||||||
|
setAlert("Código de provisionamento inválido. Confirme o segredo configurado no servidor e tente novamente.", "error")
|
||||||
|
} else {
|
||||||
|
setAlert(message, "error")
|
||||||
|
}
|
||||||
|
setStatus("Revise os dados e tente novamente.")
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToApp(config: AgentConfig) {
|
||||||
|
const url = `${config.appUrl}/machines/handshake?token=${encodeURIComponent(config.machineToken)}`
|
||||||
|
window.location.replace(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureHeartbeat(config: AgentConfig): Promise<AgentConfig> {
|
||||||
|
const adjustedConfig = {
|
||||||
|
...config,
|
||||||
|
apiBaseUrl,
|
||||||
|
appUrl,
|
||||||
|
lastSyncedAt: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveConfig(adjustedConfig)
|
||||||
|
await startHeartbeat(adjustedConfig)
|
||||||
|
|
||||||
|
return adjustedConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
setStatus("Iniciando agente desktop…")
|
||||||
|
setAlert(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = await loadConfig()
|
||||||
|
if (stored?.machineToken) {
|
||||||
|
const updated = await ensureHeartbeat(stored)
|
||||||
|
renderRegistered(updated)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[agent] Falha ao iniciar com configuração existente", error)
|
||||||
|
setAlert("Não foi possível carregar a configuração armazenada. Você poderá reprovisionar abaixo.", "error")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setStatus("Coletando informações básicas da máquina…")
|
||||||
|
const profile = await collectMachineProfile()
|
||||||
|
renderProvisionForm(profile)
|
||||||
|
setStatus("Informe o código de provisionamento para registrar esta máquina.")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[agent] Falha ao coletar dados da máquina", error)
|
||||||
|
setAlert("Não foi possível coletar dados da máquina. Verifique permissões do sistema e tente novamente.", "error")
|
||||||
|
setStatus("Interação necessária para continuar.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
void bootstrap()
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,116 +1,235 @@
|
||||||
.logo.vite:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #747bff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.typescript:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #2d79c7);
|
|
||||||
}
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
color-scheme: light dark;
|
||||||
|
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 24px;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
background-color: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 10vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
body {
|
||||||
height: 6em;
|
margin: 0;
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: 0.75s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo.tauri:hover {
|
.app-root {
|
||||||
filter: drop-shadow(0 0 2em #24c8db);
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: radial-gradient(circle at top, rgba(59, 130, 246, 0.12), transparent 60%),
|
||||||
|
radial-gradient(circle at bottom, rgba(16, 185, 129, 0.12), transparent 55%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.card {
|
||||||
display: flex;
|
width: min(440px, 100%);
|
||||||
justify-content: center;
|
background-color: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 16px 60px rgba(15, 23, 42, 0.16);
|
||||||
|
padding: 28px;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.card header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #0f172a;
|
||||||
|
background-color: #e0f2fe;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.error {
|
||||||
|
background-color: #fee2e2;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.success {
|
||||||
|
background-color: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #646cff;
|
color: #0f172a;
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
label span.optional {
|
||||||
color: #535bf2;
|
font-weight: 400;
|
||||||
}
|
color: #64748b;
|
||||||
|
font-size: 0.85rem;
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
button {
|
select {
|
||||||
border-radius: 8px;
|
padding: 10px 12px;
|
||||||
border: 1px solid transparent;
|
border-radius: 10px;
|
||||||
padding: 0.6em 1.2em;
|
border: 1px solid rgba(148, 163, 184, 0.6);
|
||||||
font-size: 1em;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
background-color: rgba(241, 245, 249, 0.8);
|
||||||
font-family: inherit;
|
color: inherit;
|
||||||
color: #0f0f0f;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
background-color: #ffffff;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
input:focus,
|
||||||
cursor: pointer;
|
select:focus {
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
border-color: #396cd8;
|
|
||||||
}
|
|
||||||
button:active {
|
|
||||||
border-color: #396cd8;
|
|
||||||
background-color: #e8e8e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border-color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
#greet-input {
|
button {
|
||||||
margin-right: 5px;
|
padding: 12px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
background-color: #2563eb;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
background: none;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background-color: #1d4ed8;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-summary {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: rgba(15, 23, 42, 0.05);
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-summary strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid rgba(37, 99, 235, 0.14);
|
||||||
|
border-top-color: #2563eb;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
color: #f6f6f6;
|
background-color: #0f172a;
|
||||||
background-color: #2f2f2f;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
.card {
|
||||||
color: #24c8db;
|
background-color: rgba(15, 23, 42, 0.75);
|
||||||
|
color: #e2e8f0;
|
||||||
|
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
background-color: rgba(37, 99, 235, 0.16);
|
||||||
|
color: #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.error {
|
||||||
|
background-color: rgba(248, 113, 113, 0.2);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.success {
|
||||||
|
background-color: rgba(34, 197, 94, 0.18);
|
||||||
|
color: #bbf7d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
button {
|
select {
|
||||||
color: #ffffff;
|
background-color: rgba(15, 23, 42, 0.5);
|
||||||
background-color: #0f0f0f98;
|
border-color: rgba(148, 163, 184, 0.35);
|
||||||
}
|
}
|
||||||
button:active {
|
|
||||||
background-color: #0f0f0f69;
|
input:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: #60a5fa;
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-summary {
|
||||||
|
background-color: rgba(148, 163, 184, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
color: #cbd5f5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,20 @@ Legenda: ✅ concluído · 🔄 em andamento · ⏳ a fazer.
|
||||||
|
|
||||||
## Notas de Implementação (Atual)
|
## Notas de Implementação (Atual)
|
||||||
- Criada pasta `apps/desktop` via `create-tauri-app` com template `vanilla-ts`.
|
- Criada pasta `apps/desktop` via `create-tauri-app` com template `vanilla-ts`.
|
||||||
- `src/main.ts` redireciona a WebView para `VITE_APP_URL` (padrão `http://localhost:3000`), reaproveitando a UI Next web.
|
- O agente desktop agora possui fluxo próprio: coleta inventário local via comandos Rust, solicita o código de provisionamento, registra a máquina e inicia heartbeats periódicos (`src-tauri/src/agent.rs` + `src/main.ts`).
|
||||||
- `index.html` exibe fallback simples enquanto o Next inicializa.
|
- Formulário inicial exibe resumo de hardware/OS e salva o token em `~/.config/Sistema de Chamados Desktop/machine-agent.json` (ou equivalente por SO) para reaproveitamento em relançamentos.
|
||||||
- Necessário criar `.env` em `apps/desktop` (ou usar variáveis de ambiente) com `VITE_APP_URL` correspondente ao ambiente.
|
- URLs configuráveis via `.env` do app desktop:
|
||||||
|
- `VITE_APP_URL` → aponta para a interface Next (padrao produção: `https://tickets.esdrasrenan.com.br`).
|
||||||
|
- `VITE_API_BASE_URL` → base usada nas chamadas REST (`/api/machines/*`), normalmente igual ao `APP_URL`.
|
||||||
|
- Após provisionar ou encontrar token válido, o agente dispara `/machines/handshake?token=...` que autentica a máquina no Better Auth, devolve cookies e redireciona para a UI.
|
||||||
|
- `apps/desktop/src-tauri/tauri.conf.json` ajustado para rodar `pnpm run dev/build`, servir `dist/` e abrir janela 1100x720.
|
||||||
- Novas tabelas Convex: `machines` (fingerprint, heartbeat, vínculo com AuthUser) e `machineTokens` (hash + TTL).
|
- Novas tabelas Convex: `machines` (fingerprint, heartbeat, vínculo com AuthUser) e `machineTokens` (hash + TTL).
|
||||||
- Novos endpoints Next:
|
- Novos endpoints Next:
|
||||||
- `POST /api/machines/register` — provisiona máquina, gera token e usuário Better Auth (role `machine`).
|
- `POST /api/machines/register` — provisiona máquina, gera token e usuário Better Auth (role `machine`).
|
||||||
- `POST /api/machines/heartbeat` — atualiza estado, métricas e renova TTL.
|
- `POST /api/machines/heartbeat` — atualiza estado, métricas e renova TTL.
|
||||||
- `POST /api/machines/sessions` — troca `machineToken` por sessão Better Auth e devolve cookies.
|
- `POST /api/machines/sessions` — troca `machineToken` por sessão Better Auth e devolve cookies.
|
||||||
|
- As rotas `/api/machines/*` respondem a preflight `OPTIONS` com CORS liberado para o agente (`https://tickets.esdrasrenan.com.br`, `tauri://localhost`, `http://localhost:1420`).
|
||||||
|
- Rota `GET /machines/handshake` realiza o login automático da máquina (seta cookies e redireciona).
|
||||||
- Webhook FleetDM: `POST /api/integrations/fleet/hosts` (header `x-fleet-secret`) sincroniza inventário/métricas utilizando `machines.upsertInventory`.
|
- Webhook FleetDM: `POST /api/integrations/fleet/hosts` (header `x-fleet-secret`) sincroniza inventário/métricas utilizando `machines.upsertInventory`.
|
||||||
- Script `ensureMachineAccount` garante usuário `AuthUser` e senha sincronizada com o token atual.
|
- Script `ensureMachineAccount` garante usuário `AuthUser` e senha sincronizada com o token atual.
|
||||||
- Variáveis `.env` novas: `MACHINE_PROVISIONING_SECRET` (obrigatória) e `MACHINE_TOKEN_TTL_MS` (opcional, padrão 30 dias).
|
- Variáveis `.env` novas: `MACHINE_PROVISIONING_SECRET` (obrigatória) e `MACHINE_TOKEN_TTL_MS` (opcional, padrão 30 dias).
|
||||||
|
|
@ -71,4 +77,5 @@ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
|
||||||
---
|
---
|
||||||
|
|
||||||
> Histórico de atualizações:
|
> Histórico de atualizações:
|
||||||
|
> - 2025-02-20 — Fluxo completo do agente desktop, heartbeats e rota `/machines/handshake` documentados (assistente).
|
||||||
> - 2025-02-14 — Documento criado com visão geral e plano macro (assistente).
|
> - 2025-02-14 — Documento criado com visão geral e plano macro (assistente).
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
"convex:dev": "convex dev",
|
"convex:dev": "convex dev",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"auth:seed": "node scripts/seed-auth.mjs",
|
"auth:seed": "node scripts/seed-auth.mjs",
|
||||||
"queues:ensure": "node scripts/ensure-default-queues.mjs"
|
"queues:ensure": "node scripts/ensure-default-queues.mjs",
|
||||||
|
"desktop:dev": "pnpm --filter appsdesktop tauri dev",
|
||||||
|
"desktop:build": "pnpm --filter appsdesktop tauri build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|
|
||||||
100
pnpm-lock.yaml
generated
100
pnpm-lock.yaml
generated
|
|
@ -195,6 +195,9 @@ importers:
|
||||||
eslint-config-next:
|
eslint-config-next:
|
||||||
specifier: 15.5.4
|
specifier: 15.5.4
|
||||||
version: 15.5.4(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)
|
version: 15.5.4(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
eslint-plugin-react-hooks:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.2.0(eslint@9.37.0(jiti@2.6.1))
|
||||||
prisma:
|
prisma:
|
||||||
specifier: ^6.16.2
|
specifier: ^6.16.2
|
||||||
version: 6.16.3(typescript@5.9.3)
|
version: 6.16.3(typescript@5.9.3)
|
||||||
|
|
@ -211,6 +214,28 @@ importers:
|
||||||
specifier: ^2.1.4
|
specifier: ^2.1.4
|
||||||
version: 2.1.9(@types/node@20.19.19)(lightningcss@1.30.1)
|
version: 2.1.9(@types/node@20.19.19)(lightningcss@1.30.1)
|
||||||
|
|
||||||
|
apps/desktop:
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api':
|
||||||
|
specifier: ^2
|
||||||
|
version: 2.8.0
|
||||||
|
'@tauri-apps/plugin-opener':
|
||||||
|
specifier: ^2
|
||||||
|
version: 2.5.0
|
||||||
|
'@tauri-apps/plugin-store':
|
||||||
|
specifier: ^2
|
||||||
|
version: 2.4.0
|
||||||
|
devDependencies:
|
||||||
|
'@tauri-apps/cli':
|
||||||
|
specifier: ^2
|
||||||
|
version: 2.8.4
|
||||||
|
typescript:
|
||||||
|
specifier: ~5.6.2
|
||||||
|
version: 5.6.3
|
||||||
|
vite:
|
||||||
|
specifier: ^6.0.3
|
||||||
|
version: 6.3.6(@types/node@20.19.19)(jiti@2.6.1)(lightningcss@1.30.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0':
|
'@alloc/quick-lru@5.2.0':
|
||||||
|
|
@ -1728,6 +1753,12 @@ packages:
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-opener@2.5.0':
|
||||||
|
resolution: {integrity: sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.0':
|
||||||
|
resolution: {integrity: sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A==}
|
||||||
|
|
||||||
'@tiptap/core@3.6.5':
|
'@tiptap/core@3.6.5':
|
||||||
resolution: {integrity: sha512-CgXuhevQbBcPfxaXzGZgIY9+aVMSAd68Q21g3EONz1iZBw026QgiaLhGK6jgGTErZL4GoNL/P+gC5nFCvN7+cA==}
|
resolution: {integrity: sha512-CgXuhevQbBcPfxaXzGZgIY9+aVMSAd68Q21g3EONz1iZBw026QgiaLhGK6jgGTErZL4GoNL/P+gC5nFCvN7+cA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -3938,6 +3969,11 @@ packages:
|
||||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
typescript@5.6.3:
|
||||||
|
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
|
|
@ -4049,6 +4085,46 @@ packages:
|
||||||
terser:
|
terser:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vite@6.3.6:
|
||||||
|
resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==}
|
||||||
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||||
|
jiti: '>=1.21.0'
|
||||||
|
less: '*'
|
||||||
|
lightningcss: ^1.21.0
|
||||||
|
sass: '*'
|
||||||
|
sass-embedded: '*'
|
||||||
|
stylus: '*'
|
||||||
|
sugarss: '*'
|
||||||
|
terser: ^5.16.0
|
||||||
|
tsx: ^4.8.1
|
||||||
|
yaml: ^2.4.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
jiti:
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
lightningcss:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
sass-embedded:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
tsx:
|
||||||
|
optional: true
|
||||||
|
yaml:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vitest@2.1.9:
|
vitest@2.1.9:
|
||||||
resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
|
resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|
@ -5432,6 +5508,14 @@ snapshots:
|
||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.8.4
|
'@tauri-apps/cli-win32-ia32-msvc': 2.8.4
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 2.8.4
|
'@tauri-apps/cli-win32-x64-msvc': 2.8.4
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-opener@2.5.0':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.8.0
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.0':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.8.0
|
||||||
|
|
||||||
'@tiptap/core@3.6.5(@tiptap/pm@3.6.5)':
|
'@tiptap/core@3.6.5(@tiptap/pm@3.6.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/pm': 3.6.5
|
'@tiptap/pm': 3.6.5
|
||||||
|
|
@ -7982,6 +8066,8 @@ snapshots:
|
||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
reflect.getprototypeof: 1.0.10
|
reflect.getprototypeof: 1.0.10
|
||||||
|
|
||||||
|
typescript@5.6.3: {}
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
uc.micro@2.1.0: {}
|
uc.micro@2.1.0: {}
|
||||||
|
|
@ -8115,6 +8201,20 @@ snapshots:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
lightningcss: 1.30.1
|
lightningcss: 1.30.1
|
||||||
|
|
||||||
|
vite@6.3.6(@types/node@20.19.19)(jiti@2.6.1)(lightningcss@1.30.1):
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.25.4
|
||||||
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
picomatch: 4.0.3
|
||||||
|
postcss: 8.5.6
|
||||||
|
rollup: 4.52.4
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 20.19.19
|
||||||
|
fsevents: 2.3.3
|
||||||
|
jiti: 2.6.1
|
||||||
|
lightningcss: 1.30.1
|
||||||
|
|
||||||
vitest@2.1.9(@types/node@20.19.19)(lightningcss@1.30.1):
|
vitest@2.1.9(@types/node@20.19.19)(lightningcss@1.30.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 2.1.9
|
'@vitest/expect': 2.1.9
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
packages:
|
packages:
|
||||||
- .
|
- .
|
||||||
|
- apps/desktop
|
||||||
|
|
||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
- '@prisma/client'
|
- '@prisma/client'
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { ConvexHttpClient } from "convex/browser"
|
import { ConvexHttpClient } from "convex/browser"
|
||||||
|
|
||||||
import { api } from "@/convex/_generated/api"
|
import { api } from "@/convex/_generated/api"
|
||||||
import { env } from "@/lib/env"
|
import { env } from "@/lib/env"
|
||||||
|
import { createCorsPreflight, jsonWithCors } from "@/server/cors"
|
||||||
|
|
||||||
const heartbeatSchema = z.object({
|
const heartbeatSchema = z.object({
|
||||||
machineToken: z.string().min(1),
|
machineToken: z.string().min(1),
|
||||||
|
|
@ -21,14 +21,20 @@ const heartbeatSchema = z.object({
|
||||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const CORS_METHODS = "POST, OPTIONS"
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return createCorsPreflight(request.headers.get("origin"), CORS_METHODS)
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
if (request.method !== "POST") {
|
if (request.method !== "POST") {
|
||||||
return NextResponse.json({ error: "Método não permitido" }, { status: 405 })
|
return jsonWithCors({ error: "Método não permitido" }, 405, request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
||||||
if (!convexUrl) {
|
if (!convexUrl) {
|
||||||
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
|
return jsonWithCors({ error: "Convex não configurado" }, 500, request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload
|
let payload
|
||||||
|
|
@ -36,16 +42,21 @@ export async function POST(request: Request) {
|
||||||
const raw = await request.json()
|
const raw = await request.json()
|
||||||
payload = heartbeatSchema.parse(raw)
|
payload = heartbeatSchema.parse(raw)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json({ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) }, { status: 400 })
|
return jsonWithCors(
|
||||||
|
{ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) },
|
||||||
|
400,
|
||||||
|
request.headers.get("origin"),
|
||||||
|
CORS_METHODS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new ConvexHttpClient(convexUrl)
|
const client = new ConvexHttpClient(convexUrl)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.mutation(api.machines.heartbeat, payload)
|
const response = await client.mutation(api.machines.heartbeat, payload)
|
||||||
return NextResponse.json(response)
|
return jsonWithCors(response, 200, request.headers.get("origin"), CORS_METHODS)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[machines.heartbeat] Falha ao registrar heartbeat", error)
|
console.error("[machines.heartbeat] Falha ao registrar heartbeat", error)
|
||||||
return NextResponse.json({ error: "Falha ao registrar heartbeat" }, { status: 500 })
|
return jsonWithCors({ error: "Falha ao registrar heartbeat" }, 500, request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { ConvexHttpClient } from "convex/browser"
|
import { ConvexHttpClient } from "convex/browser"
|
||||||
|
|
||||||
|
|
@ -7,6 +6,7 @@ import type { Id } from "@/convex/_generated/dataModel"
|
||||||
import { env } from "@/lib/env"
|
import { env } from "@/lib/env"
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||||
import { ensureMachineAccount } from "@/server/machines-auth"
|
import { ensureMachineAccount } from "@/server/machines-auth"
|
||||||
|
import { createCorsPreflight, jsonWithCors } from "@/server/cors"
|
||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
@ -29,14 +29,20 @@ const registerSchema = z
|
||||||
{ message: "Informe ao menos um MAC address ou número de série" }
|
{ message: "Informe ao menos um MAC address ou número de série" }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CORS_METHODS = "POST, OPTIONS"
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return createCorsPreflight(request.headers.get("origin"), CORS_METHODS)
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
if (request.method !== "POST") {
|
if (request.method !== "POST") {
|
||||||
return NextResponse.json({ error: "Método não permitido" }, { status: 405 })
|
return jsonWithCors({ error: "Método não permitido" }, 405, request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
||||||
if (!convexUrl) {
|
if (!convexUrl) {
|
||||||
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
|
return jsonWithCors({ error: "Convex não configurado" }, 500, request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload
|
let payload
|
||||||
|
|
@ -44,7 +50,12 @@ export async function POST(request: Request) {
|
||||||
const raw = await request.json()
|
const raw = await request.json()
|
||||||
payload = registerSchema.parse(raw)
|
payload = registerSchema.parse(raw)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json({ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) }, { status: 400 })
|
return jsonWithCors(
|
||||||
|
{ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) },
|
||||||
|
400,
|
||||||
|
request.headers.get("origin"),
|
||||||
|
CORS_METHODS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new ConvexHttpClient(convexUrl)
|
const client = new ConvexHttpClient(convexUrl)
|
||||||
|
|
@ -75,7 +86,7 @@ export async function POST(request: Request) {
|
||||||
authEmail: account.authEmail,
|
authEmail: account.authEmail,
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json(
|
return jsonWithCors(
|
||||||
{
|
{
|
||||||
machineId: registration.machineId,
|
machineId: registration.machineId,
|
||||||
tenantId: registration.tenantId,
|
tenantId: registration.tenantId,
|
||||||
|
|
@ -85,10 +96,12 @@ export async function POST(request: Request) {
|
||||||
machineEmail: account.authEmail,
|
machineEmail: account.authEmail,
|
||||||
expiresAt: registration.expiresAt,
|
expiresAt: registration.expiresAt,
|
||||||
},
|
},
|
||||||
{ status: 201 }
|
{ status: 201 },
|
||||||
|
request.headers.get("origin"),
|
||||||
|
CORS_METHODS
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[machines.register] Falha no provisionamento", error)
|
console.error("[machines.register] Falha no provisionamento", error)
|
||||||
return NextResponse.json({ error: "Falha ao provisionar máquina" }, { status: 500 })
|
return jsonWithCors({ error: "Falha ao provisionar máquina" }, 500, request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,22 @@
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { ConvexHttpClient } from "convex/browser"
|
import { createMachineSession } from "@/server/machines-session"
|
||||||
|
import { applyCorsHeaders, createCorsPreflight, jsonWithCors } from "@/server/cors"
|
||||||
import { api } from "@/convex/_generated/api"
|
|
||||||
import type { Id } from "@/convex/_generated/dataModel"
|
|
||||||
import { env } from "@/lib/env"
|
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
|
||||||
import { ensureMachineAccount } from "@/server/machines-auth"
|
|
||||||
import { auth } from "@/lib/auth"
|
|
||||||
|
|
||||||
const sessionSchema = z.object({
|
const sessionSchema = z.object({
|
||||||
machineToken: z.string().min(1),
|
machineToken: z.string().min(1),
|
||||||
rememberMe: z.boolean().optional(),
|
rememberMe: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
const CORS_METHODS = "POST, OPTIONS"
|
||||||
if (request.method !== "POST") {
|
|
||||||
return NextResponse.json({ error: "Método não permitido" }, { status: 405 })
|
export async function OPTIONS(request: Request) {
|
||||||
|
return createCorsPreflight(request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
export async function POST(request: Request) {
|
||||||
if (!convexUrl) {
|
if (request.method !== "POST") {
|
||||||
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
|
return jsonWithCors({ error: "Método não permitido" }, 405, request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload
|
let payload
|
||||||
|
|
@ -29,68 +24,34 @@ export async function POST(request: Request) {
|
||||||
const raw = await request.json()
|
const raw = await request.json()
|
||||||
payload = sessionSchema.parse(raw)
|
payload = sessionSchema.parse(raw)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json({ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) }, { status: 400 })
|
return jsonWithCors(
|
||||||
|
{ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) },
|
||||||
|
400,
|
||||||
|
request.headers.get("origin"),
|
||||||
|
CORS_METHODS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new ConvexHttpClient(convexUrl)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resolved = await client.mutation(api.machines.resolveToken, { machineToken: payload.machineToken })
|
const session = await createMachineSession(payload.machineToken, payload.rememberMe ?? true)
|
||||||
let machineEmail = resolved.machine.authEmail ?? null
|
|
||||||
|
|
||||||
if (!machineEmail) {
|
|
||||||
const account = await ensureMachineAccount({
|
|
||||||
machineId: resolved.machine._id,
|
|
||||||
tenantId: resolved.machine.tenantId ?? DEFAULT_TENANT_ID,
|
|
||||||
hostname: resolved.machine.hostname,
|
|
||||||
machineToken: payload.machineToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
await client.mutation(api.machines.linkAuthAccount, {
|
|
||||||
machineId: resolved.machine._id as Id<"machines">,
|
|
||||||
authUserId: account.authUserId,
|
|
||||||
authEmail: account.authEmail,
|
|
||||||
})
|
|
||||||
|
|
||||||
machineEmail = account.authEmail
|
|
||||||
}
|
|
||||||
|
|
||||||
const signIn = await auth.api.signInEmail({
|
|
||||||
body: {
|
|
||||||
email: machineEmail,
|
|
||||||
password: payload.machineToken,
|
|
||||||
rememberMe: payload.rememberMe ?? true,
|
|
||||||
},
|
|
||||||
returnHeaders: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = NextResponse.json(
|
const response = NextResponse.json(
|
||||||
{
|
{
|
||||||
ok: true,
|
ok: true,
|
||||||
machine: {
|
machine: session.machine,
|
||||||
id: resolved.machine._id,
|
session: session.response,
|
||||||
hostname: resolved.machine.hostname,
|
|
||||||
osName: resolved.machine.osName,
|
|
||||||
osVersion: resolved.machine.osVersion,
|
|
||||||
architecture: resolved.machine.architecture,
|
|
||||||
status: resolved.machine.status,
|
|
||||||
lastHeartbeatAt: resolved.machine.lastHeartbeatAt,
|
|
||||||
companyId: resolved.machine.companyId,
|
|
||||||
companySlug: resolved.machine.companySlug,
|
|
||||||
metadata: resolved.machine.metadata,
|
|
||||||
},
|
|
||||||
session: signIn.response,
|
|
||||||
},
|
},
|
||||||
{ status: 200 }
|
{ status: 200 }
|
||||||
)
|
)
|
||||||
|
|
||||||
signIn.headers.forEach((value, key) => {
|
session.headers.forEach((value, key) => {
|
||||||
response.headers.set(key, value)
|
response.headers.set(key, value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
applyCorsHeaders(response, request.headers.get("origin"), CORS_METHODS)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[machines.sessions] Falha ao criar sessão", error)
|
console.error("[machines.sessions] Falha ao criar sessão", error)
|
||||||
return NextResponse.json({ error: "Falha ao autenticar máquina" }, { status: 500 })
|
return jsonWithCors({ error: "Falha ao autenticar máquina" }, 500, request.headers.get("origin"), CORS_METHODS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
src/app/machines/handshake/route.ts
Normal file
64
src/app/machines/handshake/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
|
import { createMachineSession } from "@/server/machines-session"
|
||||||
|
|
||||||
|
const ERROR_TEMPLATE = `
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Falha na autenticação da máquina</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background-color: #0f172a; color: #e2e8f0; margin: 0; display: grid; place-items: center; height: 100vh; }
|
||||||
|
main { max-width: 480px; padding: 32px; border-radius: 16px; background-color: rgba(15, 23, 42, 0.65); box-shadow: 0 18px 42px rgba(15, 23, 42, 0.45); text-align: center; backdrop-filter: blur(10px); }
|
||||||
|
h1 { margin: 0 0 12px; font-size: 1.6rem; }
|
||||||
|
p { margin: 6px 0; line-height: 1.5; }
|
||||||
|
a { display: inline-block; margin-top: 18px; padding: 10px 16px; border-radius: 10px; background-color: #2563eb; color: #fff; text-decoration: none; font-weight: 600; }
|
||||||
|
a:hover { background-color: #1d4ed8; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Não foi possível autenticar esta máquina</h1>
|
||||||
|
<p>O token informado é inválido, expirou ou não está mais associado a uma máquina ativa.</p>
|
||||||
|
<p>Volte ao agente desktop, gere um novo token ou realize o provisionamento novamente.</p>
|
||||||
|
<a href="/">Voltar para o Sistema de Chamados</a>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const token = request.nextUrl.searchParams.get("token")
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.redirect(new URL("/", request.nextUrl.origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectParam = request.nextUrl.searchParams.get("redirect") ?? "/"
|
||||||
|
const redirectUrl = new URL(redirectParam, request.nextUrl.origin)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await createMachineSession(token, true)
|
||||||
|
const response = NextResponse.redirect(redirectUrl)
|
||||||
|
|
||||||
|
session.headers.forEach((value, key) => {
|
||||||
|
if (key.toLowerCase() === "set-cookie") {
|
||||||
|
response.headers.append("set-cookie", value)
|
||||||
|
} else {
|
||||||
|
response.headers.set(key, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[machines.handshake] Falha ao autenticar máquina", error)
|
||||||
|
return new NextResponse(ERROR_TEMPLATE, {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
38
src/server/cors.ts
Normal file
38
src/server/cors.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
|
const DEFAULT_ALLOWED_ORIGINS = [
|
||||||
|
process.env.NEXT_PUBLIC_APP_URL ?? null,
|
||||||
|
"https://tickets.esdrasrenan.com.br",
|
||||||
|
"http://localhost:1420",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"tauri://localhost",
|
||||||
|
].filter((value): value is string => Boolean(value))
|
||||||
|
|
||||||
|
export function resolveCorsOrigin(requestOrigin: string | null): string {
|
||||||
|
if (!requestOrigin) return "*"
|
||||||
|
const allowed = new Set(DEFAULT_ALLOWED_ORIGINS)
|
||||||
|
if (allowed.has(requestOrigin)) {
|
||||||
|
return requestOrigin
|
||||||
|
}
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyCorsHeaders(response: NextResponse, origin: string | null, methods = "POST, OPTIONS") {
|
||||||
|
const resolvedOrigin = resolveCorsOrigin(origin)
|
||||||
|
response.headers.set("Access-Control-Allow-Origin", resolvedOrigin)
|
||||||
|
response.headers.set("Access-Control-Allow-Methods", methods)
|
||||||
|
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
response.headers.set("Access-Control-Max-Age", "86400")
|
||||||
|
response.headers.set("Vary", "Origin")
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCorsPreflight(origin: string | null, methods = "POST, OPTIONS") {
|
||||||
|
const response = new NextResponse(null, { status: 204 })
|
||||||
|
return applyCorsHeaders(response, origin, methods)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonWithCors<T>(data: T, init: number | ResponseInit, origin: string | null, methods = "POST, OPTIONS") {
|
||||||
|
const response = NextResponse.json(data, typeof init === "number" ? { status: init } : init)
|
||||||
|
return applyCorsHeaders(response, origin, methods)
|
||||||
|
}
|
||||||
81
src/server/machines-session.ts
Normal file
81
src/server/machines-session.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { ConvexHttpClient } from "convex/browser"
|
||||||
|
|
||||||
|
import { api } from "@/convex/_generated/api"
|
||||||
|
import type { Id } from "@/convex/_generated/dataModel"
|
||||||
|
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||||
|
import { env } from "@/lib/env"
|
||||||
|
import { ensureMachineAccount } from "@/server/machines-auth"
|
||||||
|
import { auth } from "@/lib/auth"
|
||||||
|
|
||||||
|
export type MachineSessionContext = {
|
||||||
|
machine: {
|
||||||
|
id: Id<"machines">
|
||||||
|
hostname: string
|
||||||
|
osName: string | null
|
||||||
|
osVersion: string | null
|
||||||
|
architecture: string | null
|
||||||
|
status: string | null
|
||||||
|
lastHeartbeatAt: number | null
|
||||||
|
companyId: Id<"companies"> | null
|
||||||
|
companySlug: string | null
|
||||||
|
metadata: Record<string, unknown> | null
|
||||||
|
}
|
||||||
|
headers: Headers
|
||||||
|
response: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMachineSession(machineToken: string, rememberMe = true): Promise<MachineSessionContext> {
|
||||||
|
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
||||||
|
if (!convexUrl) {
|
||||||
|
throw new Error("Convex não configurado")
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ConvexHttpClient(convexUrl)
|
||||||
|
|
||||||
|
const resolved = await client.mutation(api.machines.resolveToken, { machineToken })
|
||||||
|
let machineEmail = resolved.machine.authEmail ?? null
|
||||||
|
|
||||||
|
if (!machineEmail) {
|
||||||
|
const account = await ensureMachineAccount({
|
||||||
|
machineId: resolved.machine._id,
|
||||||
|
tenantId: resolved.machine.tenantId ?? DEFAULT_TENANT_ID,
|
||||||
|
hostname: resolved.machine.hostname,
|
||||||
|
machineToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.mutation(api.machines.linkAuthAccount, {
|
||||||
|
machineId: resolved.machine._id as Id<"machines">,
|
||||||
|
authUserId: account.authUserId,
|
||||||
|
authEmail: account.authEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
machineEmail = account.authEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
const signIn = await auth.api.signInEmail({
|
||||||
|
body: {
|
||||||
|
email: machineEmail,
|
||||||
|
password: machineToken,
|
||||||
|
rememberMe,
|
||||||
|
},
|
||||||
|
returnHeaders: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
machine: {
|
||||||
|
id: resolved.machine._id as Id<"machines">,
|
||||||
|
hostname: resolved.machine.hostname,
|
||||||
|
osName: resolved.machine.osName,
|
||||||
|
osVersion: resolved.machine.osVersion,
|
||||||
|
architecture: resolved.machine.architecture,
|
||||||
|
status: resolved.machine.status,
|
||||||
|
lastHeartbeatAt: resolved.machine.lastHeartbeatAt,
|
||||||
|
companyId: (resolved.machine.companyId ?? null) as Id<"companies"> | null,
|
||||||
|
companySlug: resolved.machine.companySlug ?? null,
|
||||||
|
metadata: (resolved.machine.metadata ?? null) as Record<string, unknown> | null,
|
||||||
|
},
|
||||||
|
headers: signIn.headers,
|
||||||
|
response: signIn.response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue