diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-12 19:38:03 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-12 19:38:03 +0200 |
| commit | ae7ef10b67737f08a86743f687bca30a12e9e17a (patch) | |
| tree | 056539ab76b801559dc8d07ee6599220e2bd8e07 | |
| parent | architecture (diff) | |
| download | minecraft-ui-ae7ef10b67737f08a86743f687bca30a12e9e17a.tar.gz minecraft-ui-ae7ef10b67737f08a86743f687bca30a12e9e17a.zip | |
first stable
| -rw-r--r-- | Cargo.lock | 3272 | ||||
| -rw-r--r-- | Cargo.toml | 23 | ||||
| -rw-r--r-- | config.toml | 17 | ||||
| -rw-r--r-- | src/auth.rs | 194 | ||||
| -rw-r--r-- | src/config.rs | 23 | ||||
| -rw-r--r-- | src/db.rs | 107 | ||||
| -rw-r--r-- | src/docker.rs | 213 | ||||
| -rw-r--r-- | src/main.rs | 99 | ||||
| -rw-r--r-- | src/models/mod.rs | 44 | ||||
| -rw-r--r-- | src/rcon.rs | 133 | ||||
| -rw-r--r-- | src/routes/accounts.rs | 236 | ||||
| -rw-r--r-- | src/routes/backups.rs | 256 | ||||
| -rw-r--r-- | src/routes/dashboard.rs | 68 | ||||
| -rw-r--r-- | src/routes/instances.rs | 484 | ||||
| -rw-r--r-- | src/routes/mod.rs | 75 | ||||
| -rw-r--r-- | src/routes/players.rs | 131 | ||||
| -rw-r--r-- | static/htmx.min.js | 1 | ||||
| -rw-r--r-- | static/style.css | 424 | ||||
| -rw-r--r-- | templates/accounts/manage.html | 70 | ||||
| -rw-r--r-- | templates/backups/list.html | 43 | ||||
| -rw-r--r-- | templates/base.html | 37 | ||||
| -rw-r--r-- | templates/change_password.html | 24 | ||||
| -rw-r--r-- | templates/dashboard.html | 45 | ||||
| -rw-r--r-- | templates/instances/create.html | 115 | ||||
| -rw-r--r-- | templates/instances/edit.html | 62 | ||||
| -rw-r--r-- | templates/instances/list.html | 50 | ||||
| -rw-r--r-- | templates/login.html | 25 | ||||
| -rw-r--r-- | templates/players/manage.html | 69 |
28 files changed, 6338 insertions, 2 deletions
| @@ -3,5 +3,3277 @@ | |||
| 3 | version = 4 | 3 | version = 4 |
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "adler2" | ||
| 7 | version = "2.0.1" | ||
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" | ||
| 10 | |||
| 11 | [[package]] | ||
| 12 | name = "aead" | ||
| 13 | version = "0.5.2" | ||
| 14 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 15 | checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" | ||
| 16 | dependencies = [ | ||
| 17 | "crypto-common 0.1.7", | ||
| 18 | "generic-array", | ||
| 19 | ] | ||
| 20 | |||
| 21 | [[package]] | ||
| 22 | name = "aes" | ||
| 23 | version = "0.8.4" | ||
| 24 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 25 | checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" | ||
| 26 | dependencies = [ | ||
| 27 | "cfg-if", | ||
| 28 | "cipher 0.4.4", | ||
| 29 | "cpufeatures 0.2.17", | ||
| 30 | ] | ||
| 31 | |||
| 32 | [[package]] | ||
| 33 | name = "aes" | ||
| 34 | version = "0.9.1" | ||
| 35 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 36 | checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" | ||
| 37 | dependencies = [ | ||
| 38 | "cipher 0.5.2", | ||
| 39 | "cpubits", | ||
| 40 | "cpufeatures 0.3.0", | ||
| 41 | ] | ||
| 42 | |||
| 43 | [[package]] | ||
| 44 | name = "aes-gcm" | ||
| 45 | version = "0.10.3" | ||
| 46 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 47 | checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" | ||
| 48 | dependencies = [ | ||
| 49 | "aead", | ||
| 50 | "aes 0.8.4", | ||
| 51 | "cipher 0.4.4", | ||
| 52 | "ctr", | ||
| 53 | "ghash", | ||
| 54 | "subtle", | ||
| 55 | ] | ||
| 56 | |||
| 57 | [[package]] | ||
| 58 | name = "aho-corasick" | ||
| 59 | version = "1.1.4" | ||
| 60 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 61 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" | ||
| 62 | dependencies = [ | ||
| 63 | "memchr", | ||
| 64 | ] | ||
| 65 | |||
| 66 | [[package]] | ||
| 67 | name = "android_system_properties" | ||
| 68 | version = "0.1.5" | ||
| 69 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 70 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" | ||
| 71 | dependencies = [ | ||
| 72 | "libc", | ||
| 73 | ] | ||
| 74 | |||
| 75 | [[package]] | ||
| 76 | name = "argon2" | ||
| 77 | version = "0.5.3" | ||
| 78 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 79 | checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" | ||
| 80 | dependencies = [ | ||
| 81 | "base64ct", | ||
| 82 | "blake2", | ||
| 83 | "cpufeatures 0.2.17", | ||
| 84 | "password-hash", | ||
| 85 | ] | ||
| 86 | |||
| 87 | [[package]] | ||
| 88 | name = "askama" | ||
| 89 | version = "0.16.0" | ||
| 90 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 91 | checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc" | ||
| 92 | dependencies = [ | ||
| 93 | "askama_macros", | ||
| 94 | "itoa", | ||
| 95 | "percent-encoding", | ||
| 96 | "serde", | ||
| 97 | "serde_json", | ||
| 98 | ] | ||
| 99 | |||
| 100 | [[package]] | ||
| 101 | name = "askama_derive" | ||
| 102 | version = "0.16.0" | ||
| 103 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 104 | checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738" | ||
| 105 | dependencies = [ | ||
| 106 | "askama_parser", | ||
| 107 | "basic-toml", | ||
| 108 | "glob", | ||
| 109 | "memchr", | ||
| 110 | "proc-macro2", | ||
| 111 | "quote", | ||
| 112 | "rustc-hash", | ||
| 113 | "serde", | ||
| 114 | "serde_derive", | ||
| 115 | "syn", | ||
| 116 | ] | ||
| 117 | |||
| 118 | [[package]] | ||
| 119 | name = "askama_macros" | ||
| 120 | version = "0.16.0" | ||
| 121 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 122 | checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a" | ||
| 123 | dependencies = [ | ||
| 124 | "askama_derive", | ||
| 125 | ] | ||
| 126 | |||
| 127 | [[package]] | ||
| 128 | name = "askama_parser" | ||
| 129 | version = "0.16.0" | ||
| 130 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 131 | checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da" | ||
| 132 | dependencies = [ | ||
| 133 | "rustc-hash", | ||
| 134 | "serde", | ||
| 135 | "serde_derive", | ||
| 136 | "unicode-ident", | ||
| 137 | "winnow", | ||
| 138 | ] | ||
| 139 | |||
| 140 | [[package]] | ||
| 141 | name = "atomic-waker" | ||
| 142 | version = "1.1.2" | ||
| 143 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 144 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" | ||
| 145 | |||
| 146 | [[package]] | ||
| 147 | name = "autocfg" | ||
| 148 | version = "1.5.1" | ||
| 149 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 150 | checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" | ||
| 151 | |||
| 152 | [[package]] | ||
| 153 | name = "aws-lc-rs" | ||
| 154 | version = "1.17.1" | ||
| 155 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 156 | checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" | ||
| 157 | dependencies = [ | ||
| 158 | "aws-lc-sys", | ||
| 159 | "zeroize", | ||
| 160 | ] | ||
| 161 | |||
| 162 | [[package]] | ||
| 163 | name = "aws-lc-sys" | ||
| 164 | version = "0.42.0" | ||
| 165 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 166 | checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" | ||
| 167 | dependencies = [ | ||
| 168 | "cc", | ||
| 169 | "cmake", | ||
| 170 | "dunce", | ||
| 171 | "fs_extra", | ||
| 172 | "pkg-config", | ||
| 173 | ] | ||
| 174 | |||
| 175 | [[package]] | ||
| 176 | name = "axum" | ||
| 177 | version = "0.8.9" | ||
| 178 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 179 | checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" | ||
| 180 | dependencies = [ | ||
| 181 | "axum-core", | ||
| 182 | "axum-macros", | ||
| 183 | "bytes", | ||
| 184 | "form_urlencoded", | ||
| 185 | "futures-util", | ||
| 186 | "http", | ||
| 187 | "http-body", | ||
| 188 | "http-body-util", | ||
| 189 | "hyper", | ||
| 190 | "hyper-util", | ||
| 191 | "itoa", | ||
| 192 | "matchit", | ||
| 193 | "memchr", | ||
| 194 | "mime", | ||
| 195 | "percent-encoding", | ||
| 196 | "pin-project-lite", | ||
| 197 | "serde_core", | ||
| 198 | "serde_json", | ||
| 199 | "serde_path_to_error", | ||
| 200 | "serde_urlencoded", | ||
| 201 | "sync_wrapper", | ||
| 202 | "tokio", | ||
| 203 | "tower", | ||
| 204 | "tower-layer", | ||
| 205 | "tower-service", | ||
| 206 | "tracing", | ||
| 207 | ] | ||
| 208 | |||
| 209 | [[package]] | ||
| 210 | name = "axum-core" | ||
| 211 | version = "0.5.6" | ||
| 212 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 213 | checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" | ||
| 214 | dependencies = [ | ||
| 215 | "bytes", | ||
| 216 | "futures-core", | ||
| 217 | "http", | ||
| 218 | "http-body", | ||
| 219 | "http-body-util", | ||
| 220 | "mime", | ||
| 221 | "pin-project-lite", | ||
| 222 | "sync_wrapper", | ||
| 223 | "tower-layer", | ||
| 224 | "tower-service", | ||
| 225 | "tracing", | ||
| 226 | ] | ||
| 227 | |||
| 228 | [[package]] | ||
| 229 | name = "axum-extra" | ||
| 230 | version = "0.12.6" | ||
| 231 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 232 | checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" | ||
| 233 | dependencies = [ | ||
| 234 | "axum", | ||
| 235 | "axum-core", | ||
| 236 | "bytes", | ||
| 237 | "cookie", | ||
| 238 | "futures-core", | ||
| 239 | "futures-util", | ||
| 240 | "http", | ||
| 241 | "http-body", | ||
| 242 | "http-body-util", | ||
| 243 | "mime", | ||
| 244 | "pin-project-lite", | ||
| 245 | "tower-layer", | ||
| 246 | "tower-service", | ||
| 247 | "tracing", | ||
| 248 | ] | ||
| 249 | |||
| 250 | [[package]] | ||
| 251 | name = "axum-macros" | ||
| 252 | version = "0.5.1" | ||
| 253 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 254 | checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" | ||
| 255 | dependencies = [ | ||
| 256 | "proc-macro2", | ||
| 257 | "quote", | ||
| 258 | "syn", | ||
| 259 | ] | ||
| 260 | |||
| 261 | [[package]] | ||
| 262 | name = "base64" | ||
| 263 | version = "0.22.1" | ||
| 264 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 265 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" | ||
| 266 | |||
| 267 | [[package]] | ||
| 268 | name = "base64ct" | ||
| 269 | version = "1.8.3" | ||
| 270 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 271 | checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" | ||
| 272 | |||
| 273 | [[package]] | ||
| 274 | name = "basic-toml" | ||
| 275 | version = "0.1.10" | ||
| 276 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 277 | checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" | ||
| 278 | dependencies = [ | ||
| 279 | "serde", | ||
| 280 | ] | ||
| 281 | |||
| 282 | [[package]] | ||
| 283 | name = "bitflags" | ||
| 284 | version = "2.13.0" | ||
| 285 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 286 | checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" | ||
| 287 | |||
| 288 | [[package]] | ||
| 289 | name = "blake2" | ||
| 290 | version = "0.10.6" | ||
| 291 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 292 | checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" | ||
| 293 | dependencies = [ | ||
| 294 | "digest 0.10.7", | ||
| 295 | ] | ||
| 296 | |||
| 297 | [[package]] | ||
| 298 | name = "block-buffer" | ||
| 299 | version = "0.10.4" | ||
| 300 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 301 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | ||
| 302 | dependencies = [ | ||
| 303 | "generic-array", | ||
| 304 | ] | ||
| 305 | |||
| 306 | [[package]] | ||
| 307 | name = "block-buffer" | ||
| 308 | version = "0.12.1" | ||
| 309 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 310 | checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" | ||
| 311 | dependencies = [ | ||
| 312 | "hybrid-array", | ||
| 313 | "zeroize", | ||
| 314 | ] | ||
| 315 | |||
| 316 | [[package]] | ||
| 317 | name = "bollard" | ||
| 318 | version = "0.17.1" | ||
| 319 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 320 | checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" | ||
| 321 | dependencies = [ | ||
| 322 | "base64", | ||
| 323 | "bollard-stubs", | ||
| 324 | "bytes", | ||
| 325 | "futures-core", | ||
| 326 | "futures-util", | ||
| 327 | "hex", | ||
| 328 | "http", | ||
| 329 | "http-body-util", | ||
| 330 | "hyper", | ||
| 331 | "hyper-named-pipe", | ||
| 332 | "hyper-util", | ||
| 333 | "hyperlocal", | ||
| 334 | "log", | ||
| 335 | "pin-project-lite", | ||
| 336 | "serde", | ||
| 337 | "serde_derive", | ||
| 338 | "serde_json", | ||
| 339 | "serde_repr", | ||
| 340 | "serde_urlencoded", | ||
| 341 | "thiserror 1.0.69", | ||
| 342 | "tokio", | ||
| 343 | "tokio-util", | ||
| 344 | "tower-service", | ||
| 345 | "url", | ||
| 346 | "winapi", | ||
| 347 | ] | ||
| 348 | |||
| 349 | [[package]] | ||
| 350 | name = "bollard-stubs" | ||
| 351 | version = "1.45.0-rc.26.0.1" | ||
| 352 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 353 | checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" | ||
| 354 | dependencies = [ | ||
| 355 | "serde", | ||
| 356 | "serde_repr", | ||
| 357 | "serde_with", | ||
| 358 | ] | ||
| 359 | |||
| 360 | [[package]] | ||
| 361 | name = "bs58" | ||
| 362 | version = "0.5.1" | ||
| 363 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 364 | checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" | ||
| 365 | dependencies = [ | ||
| 366 | "tinyvec", | ||
| 367 | ] | ||
| 368 | |||
| 369 | [[package]] | ||
| 370 | name = "bumpalo" | ||
| 371 | version = "3.20.3" | ||
| 372 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 373 | checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" | ||
| 374 | |||
| 375 | [[package]] | ||
| 376 | name = "bytes" | ||
| 377 | version = "1.12.1" | ||
| 378 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 379 | checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" | ||
| 380 | |||
| 381 | [[package]] | ||
| 382 | name = "bzip2" | ||
| 383 | version = "0.6.1" | ||
| 384 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 385 | checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" | ||
| 386 | dependencies = [ | ||
| 387 | "libbz2-rs-sys", | ||
| 388 | ] | ||
| 389 | |||
| 390 | [[package]] | ||
| 391 | name = "cc" | ||
| 392 | version = "1.2.67" | ||
| 393 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 394 | checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" | ||
| 395 | dependencies = [ | ||
| 396 | "find-msvc-tools", | ||
| 397 | "jobserver", | ||
| 398 | "libc", | ||
| 399 | "shlex", | ||
| 400 | ] | ||
| 401 | |||
| 402 | [[package]] | ||
| 403 | name = "cfg-if" | ||
| 404 | version = "1.0.4" | ||
| 405 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 406 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" | ||
| 407 | |||
| 408 | [[package]] | ||
| 409 | name = "cfg_aliases" | ||
| 410 | version = "0.2.1" | ||
| 411 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 412 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" | ||
| 413 | |||
| 414 | [[package]] | ||
| 415 | name = "chacha20" | ||
| 416 | version = "0.10.1" | ||
| 417 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 418 | checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" | ||
| 419 | dependencies = [ | ||
| 420 | "cfg-if", | ||
| 421 | "cpufeatures 0.3.0", | ||
| 422 | "rand_core 0.10.1", | ||
| 423 | ] | ||
| 424 | |||
| 425 | [[package]] | ||
| 426 | name = "chrono" | ||
| 427 | version = "0.4.45" | ||
| 428 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 429 | checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" | ||
| 430 | dependencies = [ | ||
| 431 | "iana-time-zone", | ||
| 432 | "js-sys", | ||
| 433 | "num-traits", | ||
| 434 | "serde", | ||
| 435 | "wasm-bindgen", | ||
| 436 | "windows-link", | ||
| 437 | ] | ||
| 438 | |||
| 439 | [[package]] | ||
| 440 | name = "cipher" | ||
| 441 | version = "0.4.4" | ||
| 442 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 443 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" | ||
| 444 | dependencies = [ | ||
| 445 | "crypto-common 0.1.7", | ||
| 446 | "inout 0.1.4", | ||
| 447 | ] | ||
| 448 | |||
| 449 | [[package]] | ||
| 450 | name = "cipher" | ||
| 451 | version = "0.5.2" | ||
| 452 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 453 | checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" | ||
| 454 | dependencies = [ | ||
| 455 | "crypto-common 0.2.2", | ||
| 456 | "inout 0.2.2", | ||
| 457 | ] | ||
| 458 | |||
| 459 | [[package]] | ||
| 460 | name = "cmake" | ||
| 461 | version = "0.1.58" | ||
| 462 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 463 | checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" | ||
| 464 | dependencies = [ | ||
| 465 | "cc", | ||
| 466 | ] | ||
| 467 | |||
| 468 | [[package]] | ||
| 469 | name = "cmov" | ||
| 470 | version = "0.5.4" | ||
| 471 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 472 | checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" | ||
| 473 | |||
| 474 | [[package]] | ||
| 475 | name = "combine" | ||
| 476 | version = "4.6.7" | ||
| 477 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 478 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" | ||
| 479 | dependencies = [ | ||
| 480 | "bytes", | ||
| 481 | "memchr", | ||
| 482 | ] | ||
| 483 | |||
| 484 | [[package]] | ||
| 485 | name = "const-oid" | ||
| 486 | version = "0.10.2" | ||
| 487 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 488 | checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" | ||
| 489 | |||
| 490 | [[package]] | ||
| 491 | name = "constant_time_eq" | ||
| 492 | version = "0.4.2" | ||
| 493 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 494 | checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" | ||
| 495 | |||
| 496 | [[package]] | ||
| 497 | name = "cookie" | ||
| 498 | version = "0.18.1" | ||
| 499 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 500 | checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" | ||
| 501 | dependencies = [ | ||
| 502 | "aes-gcm", | ||
| 503 | "base64", | ||
| 504 | "percent-encoding", | ||
| 505 | "rand 0.8.7", | ||
| 506 | "subtle", | ||
| 507 | "time", | ||
| 508 | "version_check", | ||
| 509 | ] | ||
| 510 | |||
| 511 | [[package]] | ||
| 512 | name = "core-foundation" | ||
| 513 | version = "0.9.4" | ||
| 514 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 515 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" | ||
| 516 | dependencies = [ | ||
| 517 | "core-foundation-sys", | ||
| 518 | "libc", | ||
| 519 | ] | ||
| 520 | |||
| 521 | [[package]] | ||
| 522 | name = "core-foundation" | ||
| 523 | version = "0.10.1" | ||
| 524 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 525 | checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" | ||
| 526 | dependencies = [ | ||
| 527 | "core-foundation-sys", | ||
| 528 | "libc", | ||
| 529 | ] | ||
| 530 | |||
| 531 | [[package]] | ||
| 532 | name = "core-foundation-sys" | ||
| 533 | version = "0.8.7" | ||
| 534 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 535 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | ||
| 536 | |||
| 537 | [[package]] | ||
| 538 | name = "cpubits" | ||
| 539 | version = "0.1.1" | ||
| 540 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 541 | checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" | ||
| 542 | |||
| 543 | [[package]] | ||
| 544 | name = "cpufeatures" | ||
| 545 | version = "0.2.17" | ||
| 546 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 547 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | ||
| 548 | dependencies = [ | ||
| 549 | "libc", | ||
| 550 | ] | ||
| 551 | |||
| 552 | [[package]] | ||
| 553 | name = "cpufeatures" | ||
| 554 | version = "0.3.0" | ||
| 555 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 556 | checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" | ||
| 557 | dependencies = [ | ||
| 558 | "libc", | ||
| 559 | ] | ||
| 560 | |||
| 561 | [[package]] | ||
| 562 | name = "crc32fast" | ||
| 563 | version = "1.5.0" | ||
| 564 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 565 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" | ||
| 566 | dependencies = [ | ||
| 567 | "cfg-if", | ||
| 568 | ] | ||
| 569 | |||
| 570 | [[package]] | ||
| 571 | name = "crypto-common" | ||
| 572 | version = "0.1.7" | ||
| 573 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 574 | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | ||
| 575 | dependencies = [ | ||
| 576 | "generic-array", | ||
| 577 | "rand_core 0.6.4", | ||
| 578 | "typenum", | ||
| 579 | ] | ||
| 580 | |||
| 581 | [[package]] | ||
| 582 | name = "crypto-common" | ||
| 583 | version = "0.2.2" | ||
| 584 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 585 | checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" | ||
| 586 | dependencies = [ | ||
| 587 | "hybrid-array", | ||
| 588 | ] | ||
| 589 | |||
| 590 | [[package]] | ||
| 591 | name = "ctr" | ||
| 592 | version = "0.9.2" | ||
| 593 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 594 | checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" | ||
| 595 | dependencies = [ | ||
| 596 | "cipher 0.4.4", | ||
| 597 | ] | ||
| 598 | |||
| 599 | [[package]] | ||
| 600 | name = "ctutils" | ||
| 601 | version = "0.4.2" | ||
| 602 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 603 | checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" | ||
| 604 | dependencies = [ | ||
| 605 | "cmov", | ||
| 606 | ] | ||
| 607 | |||
| 608 | [[package]] | ||
| 609 | name = "deflate64" | ||
| 610 | version = "0.1.12" | ||
| 611 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 612 | checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" | ||
| 613 | |||
| 614 | [[package]] | ||
| 615 | name = "deranged" | ||
| 616 | version = "0.5.8" | ||
| 617 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 618 | checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" | ||
| 619 | dependencies = [ | ||
| 620 | "serde_core", | ||
| 621 | ] | ||
| 622 | |||
| 623 | [[package]] | ||
| 624 | name = "digest" | ||
| 625 | version = "0.10.7" | ||
| 626 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 627 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | ||
| 628 | dependencies = [ | ||
| 629 | "block-buffer 0.10.4", | ||
| 630 | "crypto-common 0.1.7", | ||
| 631 | "subtle", | ||
| 632 | ] | ||
| 633 | |||
| 634 | [[package]] | ||
| 635 | name = "digest" | ||
| 636 | version = "0.11.3" | ||
| 637 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 638 | checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" | ||
| 639 | dependencies = [ | ||
| 640 | "block-buffer 0.12.1", | ||
| 641 | "const-oid", | ||
| 642 | "crypto-common 0.2.2", | ||
| 643 | "ctutils", | ||
| 644 | "zeroize", | ||
| 645 | ] | ||
| 646 | |||
| 647 | [[package]] | ||
| 648 | name = "displaydoc" | ||
| 649 | version = "0.2.6" | ||
| 650 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 651 | checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" | ||
| 652 | dependencies = [ | ||
| 653 | "proc-macro2", | ||
| 654 | "quote", | ||
| 655 | "syn", | ||
| 656 | ] | ||
| 657 | |||
| 658 | [[package]] | ||
| 659 | name = "dunce" | ||
| 660 | version = "1.0.5" | ||
| 661 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 662 | checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" | ||
| 663 | |||
| 664 | [[package]] | ||
| 665 | name = "dyn-clone" | ||
| 666 | version = "1.0.20" | ||
| 667 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 668 | checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" | ||
| 669 | |||
| 670 | [[package]] | ||
| 671 | name = "encoding_rs" | ||
| 672 | version = "0.8.35" | ||
| 673 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 674 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" | ||
| 675 | dependencies = [ | ||
| 676 | "cfg-if", | ||
| 677 | ] | ||
| 678 | |||
| 679 | [[package]] | ||
| 680 | name = "equivalent" | ||
| 681 | version = "1.0.2" | ||
| 682 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 683 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | ||
| 684 | |||
| 685 | [[package]] | ||
| 686 | name = "errno" | ||
| 687 | version = "0.3.14" | ||
| 688 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 689 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | ||
| 690 | dependencies = [ | ||
| 691 | "libc", | ||
| 692 | "windows-sys 0.61.2", | ||
| 693 | ] | ||
| 694 | |||
| 695 | [[package]] | ||
| 696 | name = "fallible-iterator" | ||
| 697 | version = "0.3.0" | ||
| 698 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 699 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" | ||
| 700 | |||
| 701 | [[package]] | ||
| 702 | name = "fallible-streaming-iterator" | ||
| 703 | version = "0.1.9" | ||
| 704 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 705 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" | ||
| 706 | |||
| 707 | [[package]] | ||
| 708 | name = "fastrand" | ||
| 709 | version = "2.4.1" | ||
| 710 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 711 | checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" | ||
| 712 | |||
| 713 | [[package]] | ||
| 714 | name = "find-msvc-tools" | ||
| 715 | version = "0.1.9" | ||
| 716 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 717 | checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" | ||
| 718 | |||
| 719 | [[package]] | ||
| 720 | name = "flate2" | ||
| 721 | version = "1.1.9" | ||
| 722 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 723 | checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" | ||
| 724 | dependencies = [ | ||
| 725 | "miniz_oxide", | ||
| 726 | "zlib-rs", | ||
| 727 | ] | ||
| 728 | |||
| 729 | [[package]] | ||
| 730 | name = "fnv" | ||
| 731 | version = "1.0.7" | ||
| 732 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 733 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | ||
| 734 | |||
| 735 | [[package]] | ||
| 736 | name = "foldhash" | ||
| 737 | version = "0.2.0" | ||
| 738 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 739 | checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" | ||
| 740 | |||
| 741 | [[package]] | ||
| 742 | name = "form_urlencoded" | ||
| 743 | version = "1.2.2" | ||
| 744 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 745 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" | ||
| 746 | dependencies = [ | ||
| 747 | "percent-encoding", | ||
| 748 | ] | ||
| 749 | |||
| 750 | [[package]] | ||
| 751 | name = "fs_extra" | ||
| 752 | version = "1.3.0" | ||
| 753 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 754 | checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" | ||
| 755 | |||
| 756 | [[package]] | ||
| 757 | name = "futures-channel" | ||
| 758 | version = "0.3.32" | ||
| 759 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 760 | checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" | ||
| 761 | dependencies = [ | ||
| 762 | "futures-core", | ||
| 763 | ] | ||
| 764 | |||
| 765 | [[package]] | ||
| 766 | name = "futures-core" | ||
| 767 | version = "0.3.32" | ||
| 768 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 769 | checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" | ||
| 770 | |||
| 771 | [[package]] | ||
| 772 | name = "futures-macro" | ||
| 773 | version = "0.3.32" | ||
| 774 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 775 | checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" | ||
| 776 | dependencies = [ | ||
| 777 | "proc-macro2", | ||
| 778 | "quote", | ||
| 779 | "syn", | ||
| 780 | ] | ||
| 781 | |||
| 782 | [[package]] | ||
| 783 | name = "futures-sink" | ||
| 784 | version = "0.3.32" | ||
| 785 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 786 | checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" | ||
| 787 | |||
| 788 | [[package]] | ||
| 789 | name = "futures-task" | ||
| 790 | version = "0.3.32" | ||
| 791 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 792 | checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" | ||
| 793 | |||
| 794 | [[package]] | ||
| 795 | name = "futures-util" | ||
| 796 | version = "0.3.32" | ||
| 797 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 798 | checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" | ||
| 799 | dependencies = [ | ||
| 800 | "futures-core", | ||
| 801 | "futures-macro", | ||
| 802 | "futures-task", | ||
| 803 | "pin-project-lite", | ||
| 804 | "slab", | ||
| 805 | ] | ||
| 806 | |||
| 807 | [[package]] | ||
| 808 | name = "generic-array" | ||
| 809 | version = "0.14.7" | ||
| 810 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 811 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | ||
| 812 | dependencies = [ | ||
| 813 | "typenum", | ||
| 814 | "version_check", | ||
| 815 | ] | ||
| 816 | |||
| 817 | [[package]] | ||
| 818 | name = "getrandom" | ||
| 819 | version = "0.2.17" | ||
| 820 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 821 | checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" | ||
| 822 | dependencies = [ | ||
| 823 | "cfg-if", | ||
| 824 | "js-sys", | ||
| 825 | "libc", | ||
| 826 | "wasi", | ||
| 827 | "wasm-bindgen", | ||
| 828 | ] | ||
| 829 | |||
| 830 | [[package]] | ||
| 831 | name = "getrandom" | ||
| 832 | version = "0.4.3" | ||
| 833 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 834 | checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" | ||
| 835 | dependencies = [ | ||
| 836 | "cfg-if", | ||
| 837 | "js-sys", | ||
| 838 | "libc", | ||
| 839 | "r-efi", | ||
| 840 | "rand_core 0.10.1", | ||
| 841 | "wasm-bindgen", | ||
| 842 | ] | ||
| 843 | |||
| 844 | [[package]] | ||
| 845 | name = "ghash" | ||
| 846 | version = "0.5.1" | ||
| 847 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 848 | checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" | ||
| 849 | dependencies = [ | ||
| 850 | "opaque-debug", | ||
| 851 | "polyval", | ||
| 852 | ] | ||
| 853 | |||
| 854 | [[package]] | ||
| 855 | name = "glob" | ||
| 856 | version = "0.3.3" | ||
| 857 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 858 | checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" | ||
| 859 | |||
| 860 | [[package]] | ||
| 861 | name = "h2" | ||
| 862 | version = "0.4.15" | ||
| 863 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 864 | checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" | ||
| 865 | dependencies = [ | ||
| 866 | "atomic-waker", | ||
| 867 | "bytes", | ||
| 868 | "fnv", | ||
| 869 | "futures-core", | ||
| 870 | "futures-sink", | ||
| 871 | "http", | ||
| 872 | "indexmap 2.14.0", | ||
| 873 | "slab", | ||
| 874 | "tokio", | ||
| 875 | "tokio-util", | ||
| 876 | "tracing", | ||
| 877 | ] | ||
| 878 | |||
| 879 | [[package]] | ||
| 880 | name = "hashbrown" | ||
| 881 | version = "0.12.3" | ||
| 882 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 883 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" | ||
| 884 | |||
| 885 | [[package]] | ||
| 886 | name = "hashbrown" | ||
| 887 | version = "0.16.1" | ||
| 888 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 889 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | ||
| 890 | dependencies = [ | ||
| 891 | "foldhash", | ||
| 892 | ] | ||
| 893 | |||
| 894 | [[package]] | ||
| 895 | name = "hashbrown" | ||
| 896 | version = "0.17.1" | ||
| 897 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 898 | checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" | ||
| 899 | dependencies = [ | ||
| 900 | "foldhash", | ||
| 901 | ] | ||
| 902 | |||
| 903 | [[package]] | ||
| 904 | name = "hashlink" | ||
| 905 | version = "0.12.1" | ||
| 906 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 907 | checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" | ||
| 908 | dependencies = [ | ||
| 909 | "hashbrown 0.17.1", | ||
| 910 | ] | ||
| 911 | |||
| 912 | [[package]] | ||
| 913 | name = "hex" | ||
| 914 | version = "0.4.3" | ||
| 915 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 916 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" | ||
| 917 | |||
| 918 | [[package]] | ||
| 919 | name = "hmac" | ||
| 920 | version = "0.13.0" | ||
| 921 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 922 | checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" | ||
| 923 | dependencies = [ | ||
| 924 | "digest 0.11.3", | ||
| 925 | ] | ||
| 926 | |||
| 927 | [[package]] | ||
| 928 | name = "http" | ||
| 929 | version = "1.4.2" | ||
| 930 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 931 | checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" | ||
| 932 | dependencies = [ | ||
| 933 | "bytes", | ||
| 934 | "itoa", | ||
| 935 | ] | ||
| 936 | |||
| 937 | [[package]] | ||
| 938 | name = "http-body" | ||
| 939 | version = "1.0.1" | ||
| 940 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 941 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" | ||
| 942 | dependencies = [ | ||
| 943 | "bytes", | ||
| 944 | "http", | ||
| 945 | ] | ||
| 946 | |||
| 947 | [[package]] | ||
| 948 | name = "http-body-util" | ||
| 949 | version = "0.1.3" | ||
| 950 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 951 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" | ||
| 952 | dependencies = [ | ||
| 953 | "bytes", | ||
| 954 | "futures-core", | ||
| 955 | "http", | ||
| 956 | "http-body", | ||
| 957 | "pin-project-lite", | ||
| 958 | ] | ||
| 959 | |||
| 960 | [[package]] | ||
| 961 | name = "http-range-header" | ||
| 962 | version = "0.4.2" | ||
| 963 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 964 | checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" | ||
| 965 | |||
| 966 | [[package]] | ||
| 967 | name = "httparse" | ||
| 968 | version = "1.10.1" | ||
| 969 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 970 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" | ||
| 971 | |||
| 972 | [[package]] | ||
| 973 | name = "httpdate" | ||
| 974 | version = "1.0.3" | ||
| 975 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 976 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" | ||
| 977 | |||
| 978 | [[package]] | ||
| 979 | name = "hybrid-array" | ||
| 980 | version = "0.4.13" | ||
| 981 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 982 | checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" | ||
| 983 | dependencies = [ | ||
| 984 | "typenum", | ||
| 985 | ] | ||
| 986 | |||
| 987 | [[package]] | ||
| 988 | name = "hyper" | ||
| 989 | version = "1.10.1" | ||
| 990 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 991 | checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" | ||
| 992 | dependencies = [ | ||
| 993 | "atomic-waker", | ||
| 994 | "bytes", | ||
| 995 | "futures-channel", | ||
| 996 | "futures-core", | ||
| 997 | "h2", | ||
| 998 | "http", | ||
| 999 | "http-body", | ||
| 1000 | "httparse", | ||
| 1001 | "httpdate", | ||
| 1002 | "itoa", | ||
| 1003 | "pin-project-lite", | ||
| 1004 | "smallvec", | ||
| 1005 | "tokio", | ||
| 1006 | "want", | ||
| 1007 | ] | ||
| 1008 | |||
| 1009 | [[package]] | ||
| 1010 | name = "hyper-named-pipe" | ||
| 1011 | version = "0.1.0" | ||
| 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1013 | checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" | ||
| 1014 | dependencies = [ | ||
| 1015 | "hex", | ||
| 1016 | "hyper", | ||
| 1017 | "hyper-util", | ||
| 1018 | "pin-project-lite", | ||
| 1019 | "tokio", | ||
| 1020 | "tower-service", | ||
| 1021 | "winapi", | ||
| 1022 | ] | ||
| 1023 | |||
| 1024 | [[package]] | ||
| 1025 | name = "hyper-rustls" | ||
| 1026 | version = "0.27.9" | ||
| 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1028 | checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" | ||
| 1029 | dependencies = [ | ||
| 1030 | "http", | ||
| 1031 | "hyper", | ||
| 1032 | "hyper-util", | ||
| 1033 | "rustls", | ||
| 1034 | "tokio", | ||
| 1035 | "tokio-rustls", | ||
| 1036 | "tower-service", | ||
| 1037 | ] | ||
| 1038 | |||
| 1039 | [[package]] | ||
| 1040 | name = "hyper-util" | ||
| 1041 | version = "0.1.20" | ||
| 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1043 | checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" | ||
| 1044 | dependencies = [ | ||
| 1045 | "base64", | ||
| 1046 | "bytes", | ||
| 1047 | "futures-channel", | ||
| 1048 | "futures-util", | ||
| 1049 | "http", | ||
| 1050 | "http-body", | ||
| 1051 | "hyper", | ||
| 1052 | "ipnet", | ||
| 1053 | "libc", | ||
| 1054 | "percent-encoding", | ||
| 1055 | "pin-project-lite", | ||
| 1056 | "socket2", | ||
| 1057 | "system-configuration", | ||
| 1058 | "tokio", | ||
| 1059 | "tower-service", | ||
| 1060 | "tracing", | ||
| 1061 | "windows-registry", | ||
| 1062 | ] | ||
| 1063 | |||
| 1064 | [[package]] | ||
| 1065 | name = "hyperlocal" | ||
| 1066 | version = "0.9.1" | ||
| 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1068 | checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" | ||
| 1069 | dependencies = [ | ||
| 1070 | "hex", | ||
| 1071 | "http-body-util", | ||
| 1072 | "hyper", | ||
| 1073 | "hyper-util", | ||
| 1074 | "pin-project-lite", | ||
| 1075 | "tokio", | ||
| 1076 | "tower-service", | ||
| 1077 | ] | ||
| 1078 | |||
| 1079 | [[package]] | ||
| 1080 | name = "iana-time-zone" | ||
| 1081 | version = "0.1.65" | ||
| 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1083 | checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" | ||
| 1084 | dependencies = [ | ||
| 1085 | "android_system_properties", | ||
| 1086 | "core-foundation-sys", | ||
| 1087 | "iana-time-zone-haiku", | ||
| 1088 | "js-sys", | ||
| 1089 | "log", | ||
| 1090 | "wasm-bindgen", | ||
| 1091 | "windows-core", | ||
| 1092 | ] | ||
| 1093 | |||
| 1094 | [[package]] | ||
| 1095 | name = "iana-time-zone-haiku" | ||
| 1096 | version = "0.1.2" | ||
| 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1098 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" | ||
| 1099 | dependencies = [ | ||
| 1100 | "cc", | ||
| 1101 | ] | ||
| 1102 | |||
| 1103 | [[package]] | ||
| 1104 | name = "icu_collections" | ||
| 1105 | version = "2.2.0" | ||
| 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1107 | checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" | ||
| 1108 | dependencies = [ | ||
| 1109 | "displaydoc", | ||
| 1110 | "potential_utf", | ||
| 1111 | "utf8_iter", | ||
| 1112 | "yoke", | ||
| 1113 | "zerofrom", | ||
| 1114 | "zerovec", | ||
| 1115 | ] | ||
| 1116 | |||
| 1117 | [[package]] | ||
| 1118 | name = "icu_locale_core" | ||
| 1119 | version = "2.2.0" | ||
| 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1121 | checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" | ||
| 1122 | dependencies = [ | ||
| 1123 | "displaydoc", | ||
| 1124 | "litemap", | ||
| 1125 | "tinystr", | ||
| 1126 | "writeable", | ||
| 1127 | "zerovec", | ||
| 1128 | ] | ||
| 1129 | |||
| 1130 | [[package]] | ||
| 1131 | name = "icu_normalizer" | ||
| 1132 | version = "2.2.0" | ||
| 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1134 | checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" | ||
| 1135 | dependencies = [ | ||
| 1136 | "icu_collections", | ||
| 1137 | "icu_normalizer_data", | ||
| 1138 | "icu_properties", | ||
| 1139 | "icu_provider", | ||
| 1140 | "smallvec", | ||
| 1141 | "zerovec", | ||
| 1142 | ] | ||
| 1143 | |||
| 1144 | [[package]] | ||
| 1145 | name = "icu_normalizer_data" | ||
| 1146 | version = "2.2.0" | ||
| 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1148 | checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" | ||
| 1149 | |||
| 1150 | [[package]] | ||
| 1151 | name = "icu_properties" | ||
| 1152 | version = "2.2.0" | ||
| 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1154 | checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" | ||
| 1155 | dependencies = [ | ||
| 1156 | "icu_collections", | ||
| 1157 | "icu_locale_core", | ||
| 1158 | "icu_properties_data", | ||
| 1159 | "icu_provider", | ||
| 1160 | "zerotrie", | ||
| 1161 | "zerovec", | ||
| 1162 | ] | ||
| 1163 | |||
| 1164 | [[package]] | ||
| 1165 | name = "icu_properties_data" | ||
| 1166 | version = "2.2.0" | ||
| 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1168 | checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" | ||
| 1169 | |||
| 1170 | [[package]] | ||
| 1171 | name = "icu_provider" | ||
| 1172 | version = "2.2.0" | ||
| 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1174 | checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" | ||
| 1175 | dependencies = [ | ||
| 1176 | "displaydoc", | ||
| 1177 | "icu_locale_core", | ||
| 1178 | "writeable", | ||
| 1179 | "yoke", | ||
| 1180 | "zerofrom", | ||
| 1181 | "zerotrie", | ||
| 1182 | "zerovec", | ||
| 1183 | ] | ||
| 1184 | |||
| 1185 | [[package]] | ||
| 1186 | name = "idna" | ||
| 1187 | version = "1.1.0" | ||
| 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1189 | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" | ||
| 1190 | dependencies = [ | ||
| 1191 | "idna_adapter", | ||
| 1192 | "smallvec", | ||
| 1193 | "utf8_iter", | ||
| 1194 | ] | ||
| 1195 | |||
| 1196 | [[package]] | ||
| 1197 | name = "idna_adapter" | ||
| 1198 | version = "1.2.2" | ||
| 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1200 | checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" | ||
| 1201 | dependencies = [ | ||
| 1202 | "icu_normalizer", | ||
| 1203 | "icu_properties", | ||
| 1204 | ] | ||
| 1205 | |||
| 1206 | [[package]] | ||
| 1207 | name = "indexmap" | ||
| 1208 | version = "1.9.3" | ||
| 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1210 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" | ||
| 1211 | dependencies = [ | ||
| 1212 | "autocfg", | ||
| 1213 | "hashbrown 0.12.3", | ||
| 1214 | "serde", | ||
| 1215 | ] | ||
| 1216 | |||
| 1217 | [[package]] | ||
| 1218 | name = "indexmap" | ||
| 1219 | version = "2.14.0" | ||
| 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1221 | checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" | ||
| 1222 | dependencies = [ | ||
| 1223 | "equivalent", | ||
| 1224 | "hashbrown 0.17.1", | ||
| 1225 | "serde", | ||
| 1226 | "serde_core", | ||
| 1227 | ] | ||
| 1228 | |||
| 1229 | [[package]] | ||
| 1230 | name = "inout" | ||
| 1231 | version = "0.1.4" | ||
| 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1233 | checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" | ||
| 1234 | dependencies = [ | ||
| 1235 | "generic-array", | ||
| 1236 | ] | ||
| 1237 | |||
| 1238 | [[package]] | ||
| 1239 | name = "inout" | ||
| 1240 | version = "0.2.2" | ||
| 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1242 | checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" | ||
| 1243 | dependencies = [ | ||
| 1244 | "hybrid-array", | ||
| 1245 | ] | ||
| 1246 | |||
| 1247 | [[package]] | ||
| 1248 | name = "ipnet" | ||
| 1249 | version = "2.12.0" | ||
| 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1251 | checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" | ||
| 1252 | |||
| 1253 | [[package]] | ||
| 1254 | name = "itoa" | ||
| 1255 | version = "1.0.18" | ||
| 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1257 | checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" | ||
| 1258 | |||
| 1259 | [[package]] | ||
| 1260 | name = "jni" | ||
| 1261 | version = "0.22.4" | ||
| 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1263 | checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" | ||
| 1264 | dependencies = [ | ||
| 1265 | "cfg-if", | ||
| 1266 | "combine", | ||
| 1267 | "jni-macros", | ||
| 1268 | "jni-sys", | ||
| 1269 | "log", | ||
| 1270 | "simd_cesu8", | ||
| 1271 | "thiserror 2.0.18", | ||
| 1272 | "walkdir", | ||
| 1273 | "windows-link", | ||
| 1274 | ] | ||
| 1275 | |||
| 1276 | [[package]] | ||
| 1277 | name = "jni-macros" | ||
| 1278 | version = "0.22.4" | ||
| 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1280 | checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" | ||
| 1281 | dependencies = [ | ||
| 1282 | "proc-macro2", | ||
| 1283 | "quote", | ||
| 1284 | "rustc_version", | ||
| 1285 | "simd_cesu8", | ||
| 1286 | "syn", | ||
| 1287 | ] | ||
| 1288 | |||
| 1289 | [[package]] | ||
| 1290 | name = "jni-sys" | ||
| 1291 | version = "0.4.1" | ||
| 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1293 | checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" | ||
| 1294 | dependencies = [ | ||
| 1295 | "jni-sys-macros", | ||
| 1296 | ] | ||
| 1297 | |||
| 1298 | [[package]] | ||
| 1299 | name = "jni-sys-macros" | ||
| 1300 | version = "0.4.1" | ||
| 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1302 | checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" | ||
| 1303 | dependencies = [ | ||
| 1304 | "quote", | ||
| 1305 | "syn", | ||
| 1306 | ] | ||
| 1307 | |||
| 1308 | [[package]] | ||
| 1309 | name = "jobserver" | ||
| 1310 | version = "0.1.35" | ||
| 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1312 | checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" | ||
| 1313 | dependencies = [ | ||
| 1314 | "getrandom 0.4.3", | ||
| 1315 | "libc", | ||
| 1316 | ] | ||
| 1317 | |||
| 1318 | [[package]] | ||
| 1319 | name = "js-sys" | ||
| 1320 | version = "0.3.103" | ||
| 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1322 | checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" | ||
| 1323 | dependencies = [ | ||
| 1324 | "cfg-if", | ||
| 1325 | "futures-util", | ||
| 1326 | "wasm-bindgen", | ||
| 1327 | ] | ||
| 1328 | |||
| 1329 | [[package]] | ||
| 1330 | name = "lazy_static" | ||
| 1331 | version = "1.5.0" | ||
| 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1333 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" | ||
| 1334 | |||
| 1335 | [[package]] | ||
| 1336 | name = "libbz2-rs-sys" | ||
| 1337 | version = "0.2.5" | ||
| 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1339 | checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" | ||
| 1340 | |||
| 1341 | [[package]] | ||
| 1342 | name = "libc" | ||
| 1343 | version = "0.2.186" | ||
| 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1345 | checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" | ||
| 1346 | |||
| 1347 | [[package]] | ||
| 1348 | name = "libsqlite3-sys" | ||
| 1349 | version = "0.38.1" | ||
| 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1351 | checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" | ||
| 1352 | dependencies = [ | ||
| 1353 | "cc", | ||
| 1354 | "pkg-config", | ||
| 1355 | "vcpkg", | ||
| 1356 | ] | ||
| 1357 | |||
| 1358 | [[package]] | ||
| 1359 | name = "linux-raw-sys" | ||
| 1360 | version = "0.12.1" | ||
| 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1362 | checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" | ||
| 1363 | |||
| 1364 | [[package]] | ||
| 1365 | name = "litemap" | ||
| 1366 | version = "0.8.2" | ||
| 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1368 | checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" | ||
| 1369 | |||
| 1370 | [[package]] | ||
| 1371 | name = "lock_api" | ||
| 1372 | version = "0.4.14" | ||
| 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1374 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" | ||
| 1375 | dependencies = [ | ||
| 1376 | "scopeguard", | ||
| 1377 | ] | ||
| 1378 | |||
| 1379 | [[package]] | ||
| 1380 | name = "log" | ||
| 1381 | version = "0.4.33" | ||
| 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1383 | checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" | ||
| 1384 | |||
| 1385 | [[package]] | ||
| 1386 | name = "lru-slab" | ||
| 1387 | version = "0.1.2" | ||
| 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1389 | checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" | ||
| 1390 | |||
| 1391 | [[package]] | ||
| 1392 | name = "lzma-rust2" | ||
| 1393 | version = "0.16.5" | ||
| 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1395 | checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" | ||
| 1396 | dependencies = [ | ||
| 1397 | "sha2", | ||
| 1398 | ] | ||
| 1399 | |||
| 1400 | [[package]] | ||
| 1401 | name = "matchers" | ||
| 1402 | version = "0.2.0" | ||
| 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1404 | checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" | ||
| 1405 | dependencies = [ | ||
| 1406 | "regex-automata", | ||
| 1407 | ] | ||
| 1408 | |||
| 1409 | [[package]] | ||
| 1410 | name = "matchit" | ||
| 1411 | version = "0.8.4" | ||
| 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1413 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" | ||
| 1414 | |||
| 1415 | [[package]] | ||
| 1416 | name = "memchr" | ||
| 1417 | version = "2.8.3" | ||
| 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1419 | checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" | ||
| 1420 | |||
| 1421 | [[package]] | ||
| 1422 | name = "mime" | ||
| 1423 | version = "0.3.17" | ||
| 1424 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1425 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | ||
| 1426 | |||
| 1427 | [[package]] | ||
| 1428 | name = "mime_guess" | ||
| 1429 | version = "2.0.5" | ||
| 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1431 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" | ||
| 1432 | dependencies = [ | ||
| 1433 | "mime", | ||
| 1434 | "unicase", | ||
| 1435 | ] | ||
| 1436 | |||
| 1437 | [[package]] | ||
| 6 | name = "minecraft-ui" | 1438 | name = "minecraft-ui" |
| 7 | version = "0.1.0" | 1439 | version = "0.1.0" |
| 1440 | dependencies = [ | ||
| 1441 | "argon2", | ||
| 1442 | "askama", | ||
| 1443 | "axum", | ||
| 1444 | "axum-extra", | ||
| 1445 | "bollard", | ||
| 1446 | "chrono", | ||
| 1447 | "cookie", | ||
| 1448 | "futures-util", | ||
| 1449 | "rand 0.8.7", | ||
| 1450 | "reqwest", | ||
| 1451 | "rusqlite", | ||
| 1452 | "serde", | ||
| 1453 | "serde_json", | ||
| 1454 | "tempfile", | ||
| 1455 | "tokio", | ||
| 1456 | "toml", | ||
| 1457 | "tower", | ||
| 1458 | "tower-http 0.7.0", | ||
| 1459 | "tracing", | ||
| 1460 | "tracing-subscriber", | ||
| 1461 | "uuid", | ||
| 1462 | "zip", | ||
| 1463 | ] | ||
| 1464 | |||
| 1465 | [[package]] | ||
| 1466 | name = "miniz_oxide" | ||
| 1467 | version = "0.8.9" | ||
| 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1469 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" | ||
| 1470 | dependencies = [ | ||
| 1471 | "adler2", | ||
| 1472 | "simd-adler32", | ||
| 1473 | ] | ||
| 1474 | |||
| 1475 | [[package]] | ||
| 1476 | name = "mio" | ||
| 1477 | version = "1.2.1" | ||
| 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1479 | checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" | ||
| 1480 | dependencies = [ | ||
| 1481 | "libc", | ||
| 1482 | "wasi", | ||
| 1483 | "windows-sys 0.61.2", | ||
| 1484 | ] | ||
| 1485 | |||
| 1486 | [[package]] | ||
| 1487 | name = "nu-ansi-term" | ||
| 1488 | version = "0.50.3" | ||
| 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1490 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | ||
| 1491 | dependencies = [ | ||
| 1492 | "windows-sys 0.61.2", | ||
| 1493 | ] | ||
| 1494 | |||
| 1495 | [[package]] | ||
| 1496 | name = "num-conv" | ||
| 1497 | version = "0.2.2" | ||
| 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1499 | checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" | ||
| 1500 | |||
| 1501 | [[package]] | ||
| 1502 | name = "num-traits" | ||
| 1503 | version = "0.2.19" | ||
| 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1505 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" | ||
| 1506 | dependencies = [ | ||
| 1507 | "autocfg", | ||
| 1508 | ] | ||
| 1509 | |||
| 1510 | [[package]] | ||
| 1511 | name = "once_cell" | ||
| 1512 | version = "1.21.4" | ||
| 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1514 | checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" | ||
| 1515 | |||
| 1516 | [[package]] | ||
| 1517 | name = "opaque-debug" | ||
| 1518 | version = "0.3.1" | ||
| 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1520 | checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" | ||
| 1521 | |||
| 1522 | [[package]] | ||
| 1523 | name = "openssl-probe" | ||
| 1524 | version = "0.2.1" | ||
| 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1526 | checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" | ||
| 1527 | |||
| 1528 | [[package]] | ||
| 1529 | name = "parking_lot" | ||
| 1530 | version = "0.12.5" | ||
| 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1532 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" | ||
| 1533 | dependencies = [ | ||
| 1534 | "lock_api", | ||
| 1535 | "parking_lot_core", | ||
| 1536 | ] | ||
| 1537 | |||
| 1538 | [[package]] | ||
| 1539 | name = "parking_lot_core" | ||
| 1540 | version = "0.9.12" | ||
| 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1542 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" | ||
| 1543 | dependencies = [ | ||
| 1544 | "cfg-if", | ||
| 1545 | "libc", | ||
| 1546 | "redox_syscall", | ||
| 1547 | "smallvec", | ||
| 1548 | "windows-link", | ||
| 1549 | ] | ||
| 1550 | |||
| 1551 | [[package]] | ||
| 1552 | name = "password-hash" | ||
| 1553 | version = "0.5.0" | ||
| 1554 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1555 | checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" | ||
| 1556 | dependencies = [ | ||
| 1557 | "base64ct", | ||
| 1558 | "rand_core 0.6.4", | ||
| 1559 | "subtle", | ||
| 1560 | ] | ||
| 1561 | |||
| 1562 | [[package]] | ||
| 1563 | name = "pbkdf2" | ||
| 1564 | version = "0.13.0" | ||
| 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1566 | checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" | ||
| 1567 | dependencies = [ | ||
| 1568 | "digest 0.11.3", | ||
| 1569 | "hmac", | ||
| 1570 | ] | ||
| 1571 | |||
| 1572 | [[package]] | ||
| 1573 | name = "percent-encoding" | ||
| 1574 | version = "2.3.2" | ||
| 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1576 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" | ||
| 1577 | |||
| 1578 | [[package]] | ||
| 1579 | name = "pin-project-lite" | ||
| 1580 | version = "0.2.17" | ||
| 1581 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1582 | checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" | ||
| 1583 | |||
| 1584 | [[package]] | ||
| 1585 | name = "pkg-config" | ||
| 1586 | version = "0.3.33" | ||
| 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1588 | checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" | ||
| 1589 | |||
| 1590 | [[package]] | ||
| 1591 | name = "polyval" | ||
| 1592 | version = "0.6.2" | ||
| 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1594 | checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" | ||
| 1595 | dependencies = [ | ||
| 1596 | "cfg-if", | ||
| 1597 | "cpufeatures 0.2.17", | ||
| 1598 | "opaque-debug", | ||
| 1599 | "universal-hash", | ||
| 1600 | ] | ||
| 1601 | |||
| 1602 | [[package]] | ||
| 1603 | name = "potential_utf" | ||
| 1604 | version = "0.1.5" | ||
| 1605 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1606 | checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" | ||
| 1607 | dependencies = [ | ||
| 1608 | "zerovec", | ||
| 1609 | ] | ||
| 1610 | |||
| 1611 | [[package]] | ||
| 1612 | name = "powerfmt" | ||
| 1613 | version = "0.2.0" | ||
| 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1615 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" | ||
| 1616 | |||
| 1617 | [[package]] | ||
| 1618 | name = "ppmd-rust" | ||
| 1619 | version = "1.4.0" | ||
| 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1621 | checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" | ||
| 1622 | |||
| 1623 | [[package]] | ||
| 1624 | name = "ppv-lite86" | ||
| 1625 | version = "0.2.21" | ||
| 1626 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1627 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" | ||
| 1628 | dependencies = [ | ||
| 1629 | "zerocopy", | ||
| 1630 | ] | ||
| 1631 | |||
| 1632 | [[package]] | ||
| 1633 | name = "proc-macro2" | ||
| 1634 | version = "1.0.106" | ||
| 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1636 | checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" | ||
| 1637 | dependencies = [ | ||
| 1638 | "unicode-ident", | ||
| 1639 | ] | ||
| 1640 | |||
| 1641 | [[package]] | ||
| 1642 | name = "quinn" | ||
| 1643 | version = "0.11.11" | ||
| 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1645 | checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" | ||
| 1646 | dependencies = [ | ||
| 1647 | "bytes", | ||
| 1648 | "cfg_aliases", | ||
| 1649 | "pin-project-lite", | ||
| 1650 | "quinn-proto", | ||
| 1651 | "quinn-udp", | ||
| 1652 | "rustc-hash", | ||
| 1653 | "rustls", | ||
| 1654 | "socket2", | ||
| 1655 | "thiserror 2.0.18", | ||
| 1656 | "tokio", | ||
| 1657 | "tracing", | ||
| 1658 | "web-time", | ||
| 1659 | ] | ||
| 1660 | |||
| 1661 | [[package]] | ||
| 1662 | name = "quinn-proto" | ||
| 1663 | version = "0.11.16" | ||
| 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1665 | checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" | ||
| 1666 | dependencies = [ | ||
| 1667 | "aws-lc-rs", | ||
| 1668 | "bytes", | ||
| 1669 | "getrandom 0.4.3", | ||
| 1670 | "lru-slab", | ||
| 1671 | "rand 0.10.2", | ||
| 1672 | "rand_pcg", | ||
| 1673 | "ring", | ||
| 1674 | "rustc-hash", | ||
| 1675 | "rustls", | ||
| 1676 | "rustls-pki-types", | ||
| 1677 | "slab", | ||
| 1678 | "thiserror 2.0.18", | ||
| 1679 | "tinyvec", | ||
| 1680 | "tracing", | ||
| 1681 | "web-time", | ||
| 1682 | ] | ||
| 1683 | |||
| 1684 | [[package]] | ||
| 1685 | name = "quinn-udp" | ||
| 1686 | version = "0.5.15" | ||
| 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1688 | checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" | ||
| 1689 | dependencies = [ | ||
| 1690 | "cfg_aliases", | ||
| 1691 | "libc", | ||
| 1692 | "once_cell", | ||
| 1693 | "socket2", | ||
| 1694 | "tracing", | ||
| 1695 | "windows-sys 0.61.2", | ||
| 1696 | ] | ||
| 1697 | |||
| 1698 | [[package]] | ||
| 1699 | name = "quote" | ||
| 1700 | version = "1.0.46" | ||
| 1701 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1702 | checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" | ||
| 1703 | dependencies = [ | ||
| 1704 | "proc-macro2", | ||
| 1705 | ] | ||
| 1706 | |||
| 1707 | [[package]] | ||
| 1708 | name = "r-efi" | ||
| 1709 | version = "6.0.0" | ||
| 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1711 | checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | ||
| 1712 | |||
| 1713 | [[package]] | ||
| 1714 | name = "rand" | ||
| 1715 | version = "0.8.7" | ||
| 1716 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1717 | checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" | ||
| 1718 | dependencies = [ | ||
| 1719 | "libc", | ||
| 1720 | "rand_chacha", | ||
| 1721 | "rand_core 0.6.4", | ||
| 1722 | ] | ||
| 1723 | |||
| 1724 | [[package]] | ||
| 1725 | name = "rand" | ||
| 1726 | version = "0.10.2" | ||
| 1727 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1728 | checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" | ||
| 1729 | dependencies = [ | ||
| 1730 | "chacha20", | ||
| 1731 | "getrandom 0.4.3", | ||
| 1732 | "rand_core 0.10.1", | ||
| 1733 | ] | ||
| 1734 | |||
| 1735 | [[package]] | ||
| 1736 | name = "rand_chacha" | ||
| 1737 | version = "0.3.1" | ||
| 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1739 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | ||
| 1740 | dependencies = [ | ||
| 1741 | "ppv-lite86", | ||
| 1742 | "rand_core 0.6.4", | ||
| 1743 | ] | ||
| 1744 | |||
| 1745 | [[package]] | ||
| 1746 | name = "rand_core" | ||
| 1747 | version = "0.6.4" | ||
| 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1749 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | ||
| 1750 | dependencies = [ | ||
| 1751 | "getrandom 0.2.17", | ||
| 1752 | ] | ||
| 1753 | |||
| 1754 | [[package]] | ||
| 1755 | name = "rand_core" | ||
| 1756 | version = "0.10.1" | ||
| 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1758 | checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" | ||
| 1759 | |||
| 1760 | [[package]] | ||
| 1761 | name = "rand_pcg" | ||
| 1762 | version = "0.10.2" | ||
| 1763 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1764 | checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" | ||
| 1765 | dependencies = [ | ||
| 1766 | "rand_core 0.10.1", | ||
| 1767 | ] | ||
| 1768 | |||
| 1769 | [[package]] | ||
| 1770 | name = "redox_syscall" | ||
| 1771 | version = "0.5.18" | ||
| 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1773 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" | ||
| 1774 | dependencies = [ | ||
| 1775 | "bitflags", | ||
| 1776 | ] | ||
| 1777 | |||
| 1778 | [[package]] | ||
| 1779 | name = "ref-cast" | ||
| 1780 | version = "1.0.25" | ||
| 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1782 | checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" | ||
| 1783 | dependencies = [ | ||
| 1784 | "ref-cast-impl", | ||
| 1785 | ] | ||
| 1786 | |||
| 1787 | [[package]] | ||
| 1788 | name = "ref-cast-impl" | ||
| 1789 | version = "1.0.25" | ||
| 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1791 | checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" | ||
| 1792 | dependencies = [ | ||
| 1793 | "proc-macro2", | ||
| 1794 | "quote", | ||
| 1795 | "syn", | ||
| 1796 | ] | ||
| 1797 | |||
| 1798 | [[package]] | ||
| 1799 | name = "regex-automata" | ||
| 1800 | version = "0.4.15" | ||
| 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1802 | checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" | ||
| 1803 | dependencies = [ | ||
| 1804 | "aho-corasick", | ||
| 1805 | "memchr", | ||
| 1806 | "regex-syntax", | ||
| 1807 | ] | ||
| 1808 | |||
| 1809 | [[package]] | ||
| 1810 | name = "regex-syntax" | ||
| 1811 | version = "0.8.11" | ||
| 1812 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1813 | checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" | ||
| 1814 | |||
| 1815 | [[package]] | ||
| 1816 | name = "reqwest" | ||
| 1817 | version = "0.13.4" | ||
| 1818 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1819 | checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" | ||
| 1820 | dependencies = [ | ||
| 1821 | "base64", | ||
| 1822 | "bytes", | ||
| 1823 | "encoding_rs", | ||
| 1824 | "futures-core", | ||
| 1825 | "h2", | ||
| 1826 | "http", | ||
| 1827 | "http-body", | ||
| 1828 | "http-body-util", | ||
| 1829 | "hyper", | ||
| 1830 | "hyper-rustls", | ||
| 1831 | "hyper-util", | ||
| 1832 | "js-sys", | ||
| 1833 | "log", | ||
| 1834 | "mime", | ||
| 1835 | "percent-encoding", | ||
| 1836 | "pin-project-lite", | ||
| 1837 | "quinn", | ||
| 1838 | "rustls", | ||
| 1839 | "rustls-pki-types", | ||
| 1840 | "rustls-platform-verifier", | ||
| 1841 | "serde", | ||
| 1842 | "serde_json", | ||
| 1843 | "sync_wrapper", | ||
| 1844 | "tokio", | ||
| 1845 | "tokio-rustls", | ||
| 1846 | "tower", | ||
| 1847 | "tower-http 0.6.11", | ||
| 1848 | "tower-service", | ||
| 1849 | "url", | ||
| 1850 | "wasm-bindgen", | ||
| 1851 | "wasm-bindgen-futures", | ||
| 1852 | "web-sys", | ||
| 1853 | ] | ||
| 1854 | |||
| 1855 | [[package]] | ||
| 1856 | name = "ring" | ||
| 1857 | version = "0.17.14" | ||
| 1858 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1859 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" | ||
| 1860 | dependencies = [ | ||
| 1861 | "cc", | ||
| 1862 | "cfg-if", | ||
| 1863 | "getrandom 0.2.17", | ||
| 1864 | "libc", | ||
| 1865 | "untrusted", | ||
| 1866 | "windows-sys 0.52.0", | ||
| 1867 | ] | ||
| 1868 | |||
| 1869 | [[package]] | ||
| 1870 | name = "rsqlite-vfs" | ||
| 1871 | version = "0.1.1" | ||
| 1872 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1873 | checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" | ||
| 1874 | dependencies = [ | ||
| 1875 | "hashbrown 0.16.1", | ||
| 1876 | "thiserror 2.0.18", | ||
| 1877 | ] | ||
| 1878 | |||
| 1879 | [[package]] | ||
| 1880 | name = "rusqlite" | ||
| 1881 | version = "0.40.1" | ||
| 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1883 | checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" | ||
| 1884 | dependencies = [ | ||
| 1885 | "bitflags", | ||
| 1886 | "fallible-iterator", | ||
| 1887 | "fallible-streaming-iterator", | ||
| 1888 | "hashlink", | ||
| 1889 | "libsqlite3-sys", | ||
| 1890 | "smallvec", | ||
| 1891 | "sqlite-wasm-rs", | ||
| 1892 | ] | ||
| 1893 | |||
| 1894 | [[package]] | ||
| 1895 | name = "rustc-hash" | ||
| 1896 | version = "2.1.3" | ||
| 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1898 | checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" | ||
| 1899 | |||
| 1900 | [[package]] | ||
| 1901 | name = "rustc_version" | ||
| 1902 | version = "0.4.1" | ||
| 1903 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1904 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" | ||
| 1905 | dependencies = [ | ||
| 1906 | "semver", | ||
| 1907 | ] | ||
| 1908 | |||
| 1909 | [[package]] | ||
| 1910 | name = "rustix" | ||
| 1911 | version = "1.1.4" | ||
| 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1913 | checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" | ||
| 1914 | dependencies = [ | ||
| 1915 | "bitflags", | ||
| 1916 | "errno", | ||
| 1917 | "libc", | ||
| 1918 | "linux-raw-sys", | ||
| 1919 | "windows-sys 0.61.2", | ||
| 1920 | ] | ||
| 1921 | |||
| 1922 | [[package]] | ||
| 1923 | name = "rustls" | ||
| 1924 | version = "0.23.41" | ||
| 1925 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1926 | checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" | ||
| 1927 | dependencies = [ | ||
| 1928 | "aws-lc-rs", | ||
| 1929 | "once_cell", | ||
| 1930 | "rustls-pki-types", | ||
| 1931 | "rustls-webpki", | ||
| 1932 | "subtle", | ||
| 1933 | "zeroize", | ||
| 1934 | ] | ||
| 1935 | |||
| 1936 | [[package]] | ||
| 1937 | name = "rustls-native-certs" | ||
| 1938 | version = "0.8.4" | ||
| 1939 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1940 | checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" | ||
| 1941 | dependencies = [ | ||
| 1942 | "openssl-probe", | ||
| 1943 | "rustls-pki-types", | ||
| 1944 | "schannel", | ||
| 1945 | "security-framework", | ||
| 1946 | ] | ||
| 1947 | |||
| 1948 | [[package]] | ||
| 1949 | name = "rustls-pki-types" | ||
| 1950 | version = "1.15.0" | ||
| 1951 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1952 | checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" | ||
| 1953 | dependencies = [ | ||
| 1954 | "web-time", | ||
| 1955 | "zeroize", | ||
| 1956 | ] | ||
| 1957 | |||
| 1958 | [[package]] | ||
| 1959 | name = "rustls-platform-verifier" | ||
| 1960 | version = "0.7.0" | ||
| 1961 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1962 | checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" | ||
| 1963 | dependencies = [ | ||
| 1964 | "core-foundation 0.10.1", | ||
| 1965 | "core-foundation-sys", | ||
| 1966 | "jni", | ||
| 1967 | "log", | ||
| 1968 | "once_cell", | ||
| 1969 | "rustls", | ||
| 1970 | "rustls-native-certs", | ||
| 1971 | "rustls-platform-verifier-android", | ||
| 1972 | "rustls-webpki", | ||
| 1973 | "security-framework", | ||
| 1974 | "security-framework-sys", | ||
| 1975 | "webpki-root-certs", | ||
| 1976 | "windows-sys 0.61.2", | ||
| 1977 | ] | ||
| 1978 | |||
| 1979 | [[package]] | ||
| 1980 | name = "rustls-platform-verifier-android" | ||
| 1981 | version = "0.1.1" | ||
| 1982 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1983 | checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" | ||
| 1984 | |||
| 1985 | [[package]] | ||
| 1986 | name = "rustls-webpki" | ||
| 1987 | version = "0.103.13" | ||
| 1988 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1989 | checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" | ||
| 1990 | dependencies = [ | ||
| 1991 | "aws-lc-rs", | ||
| 1992 | "ring", | ||
| 1993 | "rustls-pki-types", | ||
| 1994 | "untrusted", | ||
| 1995 | ] | ||
| 1996 | |||
| 1997 | [[package]] | ||
| 1998 | name = "rustversion" | ||
| 1999 | version = "1.0.23" | ||
| 2000 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2001 | checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" | ||
| 2002 | |||
| 2003 | [[package]] | ||
| 2004 | name = "ryu" | ||
| 2005 | version = "1.0.23" | ||
| 2006 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2007 | checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" | ||
| 2008 | |||
| 2009 | [[package]] | ||
| 2010 | name = "same-file" | ||
| 2011 | version = "1.0.6" | ||
| 2012 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2013 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" | ||
| 2014 | dependencies = [ | ||
| 2015 | "winapi-util", | ||
| 2016 | ] | ||
| 2017 | |||
| 2018 | [[package]] | ||
| 2019 | name = "schannel" | ||
| 2020 | version = "0.1.29" | ||
| 2021 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2022 | checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" | ||
| 2023 | dependencies = [ | ||
| 2024 | "windows-sys 0.61.2", | ||
| 2025 | ] | ||
| 2026 | |||
| 2027 | [[package]] | ||
| 2028 | name = "schemars" | ||
| 2029 | version = "0.9.0" | ||
| 2030 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2031 | checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" | ||
| 2032 | dependencies = [ | ||
| 2033 | "dyn-clone", | ||
| 2034 | "ref-cast", | ||
| 2035 | "serde", | ||
| 2036 | "serde_json", | ||
| 2037 | ] | ||
| 2038 | |||
| 2039 | [[package]] | ||
| 2040 | name = "schemars" | ||
| 2041 | version = "1.2.1" | ||
| 2042 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2043 | checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" | ||
| 2044 | dependencies = [ | ||
| 2045 | "dyn-clone", | ||
| 2046 | "ref-cast", | ||
| 2047 | "serde", | ||
| 2048 | "serde_json", | ||
| 2049 | ] | ||
| 2050 | |||
| 2051 | [[package]] | ||
| 2052 | name = "scopeguard" | ||
| 2053 | version = "1.2.0" | ||
| 2054 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2055 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | ||
| 2056 | |||
| 2057 | [[package]] | ||
| 2058 | name = "security-framework" | ||
| 2059 | version = "3.7.0" | ||
| 2060 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2061 | checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" | ||
| 2062 | dependencies = [ | ||
| 2063 | "bitflags", | ||
| 2064 | "core-foundation 0.10.1", | ||
| 2065 | "core-foundation-sys", | ||
| 2066 | "libc", | ||
| 2067 | "security-framework-sys", | ||
| 2068 | ] | ||
| 2069 | |||
| 2070 | [[package]] | ||
| 2071 | name = "security-framework-sys" | ||
| 2072 | version = "2.17.0" | ||
| 2073 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2074 | checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" | ||
| 2075 | dependencies = [ | ||
| 2076 | "core-foundation-sys", | ||
| 2077 | "libc", | ||
| 2078 | ] | ||
| 2079 | |||
| 2080 | [[package]] | ||
| 2081 | name = "semver" | ||
| 2082 | version = "1.0.28" | ||
| 2083 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2084 | checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" | ||
| 2085 | |||
| 2086 | [[package]] | ||
| 2087 | name = "serde" | ||
| 2088 | version = "1.0.228" | ||
| 2089 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2090 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" | ||
| 2091 | dependencies = [ | ||
| 2092 | "serde_core", | ||
| 2093 | "serde_derive", | ||
| 2094 | ] | ||
| 2095 | |||
| 2096 | [[package]] | ||
| 2097 | name = "serde_core" | ||
| 2098 | version = "1.0.228" | ||
| 2099 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2100 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" | ||
| 2101 | dependencies = [ | ||
| 2102 | "serde_derive", | ||
| 2103 | ] | ||
| 2104 | |||
| 2105 | [[package]] | ||
| 2106 | name = "serde_derive" | ||
| 2107 | version = "1.0.228" | ||
| 2108 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2109 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" | ||
| 2110 | dependencies = [ | ||
| 2111 | "proc-macro2", | ||
| 2112 | "quote", | ||
| 2113 | "syn", | ||
| 2114 | ] | ||
| 2115 | |||
| 2116 | [[package]] | ||
| 2117 | name = "serde_json" | ||
| 2118 | version = "1.0.150" | ||
| 2119 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2120 | checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" | ||
| 2121 | dependencies = [ | ||
| 2122 | "itoa", | ||
| 2123 | "memchr", | ||
| 2124 | "serde", | ||
| 2125 | "serde_core", | ||
| 2126 | "zmij", | ||
| 2127 | ] | ||
| 2128 | |||
| 2129 | [[package]] | ||
| 2130 | name = "serde_path_to_error" | ||
| 2131 | version = "0.1.20" | ||
| 2132 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2133 | checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" | ||
| 2134 | dependencies = [ | ||
| 2135 | "itoa", | ||
| 2136 | "serde", | ||
| 2137 | "serde_core", | ||
| 2138 | ] | ||
| 2139 | |||
| 2140 | [[package]] | ||
| 2141 | name = "serde_repr" | ||
| 2142 | version = "0.1.20" | ||
| 2143 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2144 | checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" | ||
| 2145 | dependencies = [ | ||
| 2146 | "proc-macro2", | ||
| 2147 | "quote", | ||
| 2148 | "syn", | ||
| 2149 | ] | ||
| 2150 | |||
| 2151 | [[package]] | ||
| 2152 | name = "serde_spanned" | ||
| 2153 | version = "1.1.1" | ||
| 2154 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2155 | checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" | ||
| 2156 | dependencies = [ | ||
| 2157 | "serde_core", | ||
| 2158 | ] | ||
| 2159 | |||
| 2160 | [[package]] | ||
| 2161 | name = "serde_urlencoded" | ||
| 2162 | version = "0.7.1" | ||
| 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2164 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" | ||
| 2165 | dependencies = [ | ||
| 2166 | "form_urlencoded", | ||
| 2167 | "itoa", | ||
| 2168 | "ryu", | ||
| 2169 | "serde", | ||
| 2170 | ] | ||
| 2171 | |||
| 2172 | [[package]] | ||
| 2173 | name = "serde_with" | ||
| 2174 | version = "3.21.0" | ||
| 2175 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2176 | checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" | ||
| 2177 | dependencies = [ | ||
| 2178 | "base64", | ||
| 2179 | "bs58", | ||
| 2180 | "chrono", | ||
| 2181 | "hex", | ||
| 2182 | "indexmap 1.9.3", | ||
| 2183 | "indexmap 2.14.0", | ||
| 2184 | "schemars 0.9.0", | ||
| 2185 | "schemars 1.2.1", | ||
| 2186 | "serde_core", | ||
| 2187 | "serde_json", | ||
| 2188 | "time", | ||
| 2189 | ] | ||
| 2190 | |||
| 2191 | [[package]] | ||
| 2192 | name = "sha1" | ||
| 2193 | version = "0.11.0" | ||
| 2194 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2195 | checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" | ||
| 2196 | dependencies = [ | ||
| 2197 | "cfg-if", | ||
| 2198 | "cpufeatures 0.3.0", | ||
| 2199 | "digest 0.11.3", | ||
| 2200 | ] | ||
| 2201 | |||
| 2202 | [[package]] | ||
| 2203 | name = "sha2" | ||
| 2204 | version = "0.11.0" | ||
| 2205 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2206 | checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" | ||
| 2207 | dependencies = [ | ||
| 2208 | "cfg-if", | ||
| 2209 | "cpufeatures 0.3.0", | ||
| 2210 | "digest 0.11.3", | ||
| 2211 | ] | ||
| 2212 | |||
| 2213 | [[package]] | ||
| 2214 | name = "sharded-slab" | ||
| 2215 | version = "0.1.7" | ||
| 2216 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2217 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" | ||
| 2218 | dependencies = [ | ||
| 2219 | "lazy_static", | ||
| 2220 | ] | ||
| 2221 | |||
| 2222 | [[package]] | ||
| 2223 | name = "shlex" | ||
| 2224 | version = "2.0.1" | ||
| 2225 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2226 | checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" | ||
| 2227 | |||
| 2228 | [[package]] | ||
| 2229 | name = "signal-hook-registry" | ||
| 2230 | version = "1.4.8" | ||
| 2231 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2232 | checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" | ||
| 2233 | dependencies = [ | ||
| 2234 | "errno", | ||
| 2235 | "libc", | ||
| 2236 | ] | ||
| 2237 | |||
| 2238 | [[package]] | ||
| 2239 | name = "simd-adler32" | ||
| 2240 | version = "0.3.9" | ||
| 2241 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2242 | checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" | ||
| 2243 | |||
| 2244 | [[package]] | ||
| 2245 | name = "simd_cesu8" | ||
| 2246 | version = "1.1.1" | ||
| 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2248 | checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" | ||
| 2249 | dependencies = [ | ||
| 2250 | "rustc_version", | ||
| 2251 | "simdutf8", | ||
| 2252 | ] | ||
| 2253 | |||
| 2254 | [[package]] | ||
| 2255 | name = "simdutf8" | ||
| 2256 | version = "0.1.5" | ||
| 2257 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2258 | checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" | ||
| 2259 | |||
| 2260 | [[package]] | ||
| 2261 | name = "slab" | ||
| 2262 | version = "0.4.12" | ||
| 2263 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2264 | checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" | ||
| 2265 | |||
| 2266 | [[package]] | ||
| 2267 | name = "smallvec" | ||
| 2268 | version = "1.15.2" | ||
| 2269 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2270 | checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" | ||
| 2271 | |||
| 2272 | [[package]] | ||
| 2273 | name = "socket2" | ||
| 2274 | version = "0.6.4" | ||
| 2275 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2276 | checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" | ||
| 2277 | dependencies = [ | ||
| 2278 | "libc", | ||
| 2279 | "windows-sys 0.61.2", | ||
| 2280 | ] | ||
| 2281 | |||
| 2282 | [[package]] | ||
| 2283 | name = "sqlite-wasm-rs" | ||
| 2284 | version = "0.5.5" | ||
| 2285 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2286 | checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" | ||
| 2287 | dependencies = [ | ||
| 2288 | "cc", | ||
| 2289 | "js-sys", | ||
| 2290 | "rsqlite-vfs", | ||
| 2291 | "wasm-bindgen", | ||
| 2292 | ] | ||
| 2293 | |||
| 2294 | [[package]] | ||
| 2295 | name = "stable_deref_trait" | ||
| 2296 | version = "1.2.1" | ||
| 2297 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2298 | checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" | ||
| 2299 | |||
| 2300 | [[package]] | ||
| 2301 | name = "subtle" | ||
| 2302 | version = "2.6.1" | ||
| 2303 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2304 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" | ||
| 2305 | |||
| 2306 | [[package]] | ||
| 2307 | name = "syn" | ||
| 2308 | version = "2.0.118" | ||
| 2309 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2310 | checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" | ||
| 2311 | dependencies = [ | ||
| 2312 | "proc-macro2", | ||
| 2313 | "quote", | ||
| 2314 | "unicode-ident", | ||
| 2315 | ] | ||
| 2316 | |||
| 2317 | [[package]] | ||
| 2318 | name = "sync_wrapper" | ||
| 2319 | version = "1.0.2" | ||
| 2320 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2321 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" | ||
| 2322 | dependencies = [ | ||
| 2323 | "futures-core", | ||
| 2324 | ] | ||
| 2325 | |||
| 2326 | [[package]] | ||
| 2327 | name = "synstructure" | ||
| 2328 | version = "0.13.2" | ||
| 2329 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2330 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" | ||
| 2331 | dependencies = [ | ||
| 2332 | "proc-macro2", | ||
| 2333 | "quote", | ||
| 2334 | "syn", | ||
| 2335 | ] | ||
| 2336 | |||
| 2337 | [[package]] | ||
| 2338 | name = "system-configuration" | ||
| 2339 | version = "0.7.0" | ||
| 2340 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2341 | checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" | ||
| 2342 | dependencies = [ | ||
| 2343 | "bitflags", | ||
| 2344 | "core-foundation 0.9.4", | ||
| 2345 | "system-configuration-sys", | ||
| 2346 | ] | ||
| 2347 | |||
| 2348 | [[package]] | ||
| 2349 | name = "system-configuration-sys" | ||
| 2350 | version = "0.6.0" | ||
| 2351 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2352 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" | ||
| 2353 | dependencies = [ | ||
| 2354 | "core-foundation-sys", | ||
| 2355 | "libc", | ||
| 2356 | ] | ||
| 2357 | |||
| 2358 | [[package]] | ||
| 2359 | name = "tempfile" | ||
| 2360 | version = "3.27.0" | ||
| 2361 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2362 | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" | ||
| 2363 | dependencies = [ | ||
| 2364 | "fastrand", | ||
| 2365 | "getrandom 0.4.3", | ||
| 2366 | "once_cell", | ||
| 2367 | "rustix", | ||
| 2368 | "windows-sys 0.61.2", | ||
| 2369 | ] | ||
| 2370 | |||
| 2371 | [[package]] | ||
| 2372 | name = "thiserror" | ||
| 2373 | version = "1.0.69" | ||
| 2374 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2375 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" | ||
| 2376 | dependencies = [ | ||
| 2377 | "thiserror-impl 1.0.69", | ||
| 2378 | ] | ||
| 2379 | |||
| 2380 | [[package]] | ||
| 2381 | name = "thiserror" | ||
| 2382 | version = "2.0.18" | ||
| 2383 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2384 | checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" | ||
| 2385 | dependencies = [ | ||
| 2386 | "thiserror-impl 2.0.18", | ||
| 2387 | ] | ||
| 2388 | |||
| 2389 | [[package]] | ||
| 2390 | name = "thiserror-impl" | ||
| 2391 | version = "1.0.69" | ||
| 2392 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2393 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" | ||
| 2394 | dependencies = [ | ||
| 2395 | "proc-macro2", | ||
| 2396 | "quote", | ||
| 2397 | "syn", | ||
| 2398 | ] | ||
| 2399 | |||
| 2400 | [[package]] | ||
| 2401 | name = "thiserror-impl" | ||
| 2402 | version = "2.0.18" | ||
| 2403 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2404 | checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" | ||
| 2405 | dependencies = [ | ||
| 2406 | "proc-macro2", | ||
| 2407 | "quote", | ||
| 2408 | "syn", | ||
| 2409 | ] | ||
| 2410 | |||
| 2411 | [[package]] | ||
| 2412 | name = "thread_local" | ||
| 2413 | version = "1.1.10" | ||
| 2414 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2415 | checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" | ||
| 2416 | dependencies = [ | ||
| 2417 | "cfg-if", | ||
| 2418 | ] | ||
| 2419 | |||
| 2420 | [[package]] | ||
| 2421 | name = "time" | ||
| 2422 | version = "0.3.53" | ||
| 2423 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2424 | checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" | ||
| 2425 | dependencies = [ | ||
| 2426 | "deranged", | ||
| 2427 | "js-sys", | ||
| 2428 | "num-conv", | ||
| 2429 | "powerfmt", | ||
| 2430 | "serde_core", | ||
| 2431 | "time-core", | ||
| 2432 | "time-macros", | ||
| 2433 | ] | ||
| 2434 | |||
| 2435 | [[package]] | ||
| 2436 | name = "time-core" | ||
| 2437 | version = "0.1.9" | ||
| 2438 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2439 | checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" | ||
| 2440 | |||
| 2441 | [[package]] | ||
| 2442 | name = "time-macros" | ||
| 2443 | version = "0.2.31" | ||
| 2444 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2445 | checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" | ||
| 2446 | dependencies = [ | ||
| 2447 | "num-conv", | ||
| 2448 | "time-core", | ||
| 2449 | ] | ||
| 2450 | |||
| 2451 | [[package]] | ||
| 2452 | name = "tinystr" | ||
| 2453 | version = "0.8.3" | ||
| 2454 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2455 | checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" | ||
| 2456 | dependencies = [ | ||
| 2457 | "displaydoc", | ||
| 2458 | "zerovec", | ||
| 2459 | ] | ||
| 2460 | |||
| 2461 | [[package]] | ||
| 2462 | name = "tinyvec" | ||
| 2463 | version = "1.12.0" | ||
| 2464 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2465 | checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" | ||
| 2466 | dependencies = [ | ||
| 2467 | "tinyvec_macros", | ||
| 2468 | ] | ||
| 2469 | |||
| 2470 | [[package]] | ||
| 2471 | name = "tinyvec_macros" | ||
| 2472 | version = "0.1.1" | ||
| 2473 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2474 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" | ||
| 2475 | |||
| 2476 | [[package]] | ||
| 2477 | name = "tokio" | ||
| 2478 | version = "1.52.3" | ||
| 2479 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2480 | checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" | ||
| 2481 | dependencies = [ | ||
| 2482 | "bytes", | ||
| 2483 | "libc", | ||
| 2484 | "mio", | ||
| 2485 | "parking_lot", | ||
| 2486 | "pin-project-lite", | ||
| 2487 | "signal-hook-registry", | ||
| 2488 | "socket2", | ||
| 2489 | "tokio-macros", | ||
| 2490 | "windows-sys 0.61.2", | ||
| 2491 | ] | ||
| 2492 | |||
| 2493 | [[package]] | ||
| 2494 | name = "tokio-macros" | ||
| 2495 | version = "2.7.0" | ||
| 2496 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2497 | checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" | ||
| 2498 | dependencies = [ | ||
| 2499 | "proc-macro2", | ||
| 2500 | "quote", | ||
| 2501 | "syn", | ||
| 2502 | ] | ||
| 2503 | |||
| 2504 | [[package]] | ||
| 2505 | name = "tokio-rustls" | ||
| 2506 | version = "0.26.4" | ||
| 2507 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2508 | checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" | ||
| 2509 | dependencies = [ | ||
| 2510 | "rustls", | ||
| 2511 | "tokio", | ||
| 2512 | ] | ||
| 2513 | |||
| 2514 | [[package]] | ||
| 2515 | name = "tokio-util" | ||
| 2516 | version = "0.7.18" | ||
| 2517 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2518 | checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" | ||
| 2519 | dependencies = [ | ||
| 2520 | "bytes", | ||
| 2521 | "futures-core", | ||
| 2522 | "futures-sink", | ||
| 2523 | "pin-project-lite", | ||
| 2524 | "tokio", | ||
| 2525 | ] | ||
| 2526 | |||
| 2527 | [[package]] | ||
| 2528 | name = "toml" | ||
| 2529 | version = "1.1.2+spec-1.1.0" | ||
| 2530 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2531 | checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" | ||
| 2532 | dependencies = [ | ||
| 2533 | "indexmap 2.14.0", | ||
| 2534 | "serde_core", | ||
| 2535 | "serde_spanned", | ||
| 2536 | "toml_datetime", | ||
| 2537 | "toml_parser", | ||
| 2538 | "toml_writer", | ||
| 2539 | "winnow", | ||
| 2540 | ] | ||
| 2541 | |||
| 2542 | [[package]] | ||
| 2543 | name = "toml_datetime" | ||
| 2544 | version = "1.1.1+spec-1.1.0" | ||
| 2545 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2546 | checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" | ||
| 2547 | dependencies = [ | ||
| 2548 | "serde_core", | ||
| 2549 | ] | ||
| 2550 | |||
| 2551 | [[package]] | ||
| 2552 | name = "toml_parser" | ||
| 2553 | version = "1.1.2+spec-1.1.0" | ||
| 2554 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2555 | checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" | ||
| 2556 | dependencies = [ | ||
| 2557 | "winnow", | ||
| 2558 | ] | ||
| 2559 | |||
| 2560 | [[package]] | ||
| 2561 | name = "toml_writer" | ||
| 2562 | version = "1.1.1+spec-1.1.0" | ||
| 2563 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2564 | checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" | ||
| 2565 | |||
| 2566 | [[package]] | ||
| 2567 | name = "tower" | ||
| 2568 | version = "0.5.3" | ||
| 2569 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2570 | checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" | ||
| 2571 | dependencies = [ | ||
| 2572 | "futures-core", | ||
| 2573 | "futures-util", | ||
| 2574 | "pin-project-lite", | ||
| 2575 | "sync_wrapper", | ||
| 2576 | "tokio", | ||
| 2577 | "tower-layer", | ||
| 2578 | "tower-service", | ||
| 2579 | "tracing", | ||
| 2580 | ] | ||
| 2581 | |||
| 2582 | [[package]] | ||
| 2583 | name = "tower-http" | ||
| 2584 | version = "0.6.11" | ||
| 2585 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2586 | checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" | ||
| 2587 | dependencies = [ | ||
| 2588 | "bitflags", | ||
| 2589 | "bytes", | ||
| 2590 | "futures-util", | ||
| 2591 | "http", | ||
| 2592 | "http-body", | ||
| 2593 | "pin-project-lite", | ||
| 2594 | "tower", | ||
| 2595 | "tower-layer", | ||
| 2596 | "tower-service", | ||
| 2597 | "url", | ||
| 2598 | ] | ||
| 2599 | |||
| 2600 | [[package]] | ||
| 2601 | name = "tower-http" | ||
| 2602 | version = "0.7.0" | ||
| 2603 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2604 | checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" | ||
| 2605 | dependencies = [ | ||
| 2606 | "bitflags", | ||
| 2607 | "bytes", | ||
| 2608 | "futures-core", | ||
| 2609 | "futures-util", | ||
| 2610 | "http", | ||
| 2611 | "http-body", | ||
| 2612 | "http-body-util", | ||
| 2613 | "http-range-header", | ||
| 2614 | "httpdate", | ||
| 2615 | "mime", | ||
| 2616 | "mime_guess", | ||
| 2617 | "percent-encoding", | ||
| 2618 | "pin-project-lite", | ||
| 2619 | "tokio", | ||
| 2620 | "tokio-util", | ||
| 2621 | "tower-layer", | ||
| 2622 | "tower-service", | ||
| 2623 | ] | ||
| 2624 | |||
| 2625 | [[package]] | ||
| 2626 | name = "tower-layer" | ||
| 2627 | version = "0.3.3" | ||
| 2628 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2629 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" | ||
| 2630 | |||
| 2631 | [[package]] | ||
| 2632 | name = "tower-service" | ||
| 2633 | version = "0.3.3" | ||
| 2634 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2635 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" | ||
| 2636 | |||
| 2637 | [[package]] | ||
| 2638 | name = "tracing" | ||
| 2639 | version = "0.1.44" | ||
| 2640 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2641 | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | ||
| 2642 | dependencies = [ | ||
| 2643 | "log", | ||
| 2644 | "pin-project-lite", | ||
| 2645 | "tracing-attributes", | ||
| 2646 | "tracing-core", | ||
| 2647 | ] | ||
| 2648 | |||
| 2649 | [[package]] | ||
| 2650 | name = "tracing-attributes" | ||
| 2651 | version = "0.1.31" | ||
| 2652 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2653 | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" | ||
| 2654 | dependencies = [ | ||
| 2655 | "proc-macro2", | ||
| 2656 | "quote", | ||
| 2657 | "syn", | ||
| 2658 | ] | ||
| 2659 | |||
| 2660 | [[package]] | ||
| 2661 | name = "tracing-core" | ||
| 2662 | version = "0.1.36" | ||
| 2663 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2664 | checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" | ||
| 2665 | dependencies = [ | ||
| 2666 | "once_cell", | ||
| 2667 | "valuable", | ||
| 2668 | ] | ||
| 2669 | |||
| 2670 | [[package]] | ||
| 2671 | name = "tracing-log" | ||
| 2672 | version = "0.2.0" | ||
| 2673 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2674 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" | ||
| 2675 | dependencies = [ | ||
| 2676 | "log", | ||
| 2677 | "once_cell", | ||
| 2678 | "tracing-core", | ||
| 2679 | ] | ||
| 2680 | |||
| 2681 | [[package]] | ||
| 2682 | name = "tracing-subscriber" | ||
| 2683 | version = "0.3.23" | ||
| 2684 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2685 | checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" | ||
| 2686 | dependencies = [ | ||
| 2687 | "matchers", | ||
| 2688 | "nu-ansi-term", | ||
| 2689 | "once_cell", | ||
| 2690 | "regex-automata", | ||
| 2691 | "sharded-slab", | ||
| 2692 | "smallvec", | ||
| 2693 | "thread_local", | ||
| 2694 | "tracing", | ||
| 2695 | "tracing-core", | ||
| 2696 | "tracing-log", | ||
| 2697 | ] | ||
| 2698 | |||
| 2699 | [[package]] | ||
| 2700 | name = "try-lock" | ||
| 2701 | version = "0.2.5" | ||
| 2702 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2703 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" | ||
| 2704 | |||
| 2705 | [[package]] | ||
| 2706 | name = "typed-path" | ||
| 2707 | version = "0.12.3" | ||
| 2708 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2709 | checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" | ||
| 2710 | |||
| 2711 | [[package]] | ||
| 2712 | name = "typenum" | ||
| 2713 | version = "1.20.1" | ||
| 2714 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2715 | checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" | ||
| 2716 | |||
| 2717 | [[package]] | ||
| 2718 | name = "unicase" | ||
| 2719 | version = "2.9.0" | ||
| 2720 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2721 | checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" | ||
| 2722 | |||
| 2723 | [[package]] | ||
| 2724 | name = "unicode-ident" | ||
| 2725 | version = "1.0.24" | ||
| 2726 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2727 | checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" | ||
| 2728 | |||
| 2729 | [[package]] | ||
| 2730 | name = "universal-hash" | ||
| 2731 | version = "0.5.1" | ||
| 2732 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2733 | checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" | ||
| 2734 | dependencies = [ | ||
| 2735 | "crypto-common 0.1.7", | ||
| 2736 | "subtle", | ||
| 2737 | ] | ||
| 2738 | |||
| 2739 | [[package]] | ||
| 2740 | name = "untrusted" | ||
| 2741 | version = "0.9.0" | ||
| 2742 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2743 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" | ||
| 2744 | |||
| 2745 | [[package]] | ||
| 2746 | name = "url" | ||
| 2747 | version = "2.5.8" | ||
| 2748 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2749 | checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" | ||
| 2750 | dependencies = [ | ||
| 2751 | "form_urlencoded", | ||
| 2752 | "idna", | ||
| 2753 | "percent-encoding", | ||
| 2754 | "serde", | ||
| 2755 | ] | ||
| 2756 | |||
| 2757 | [[package]] | ||
| 2758 | name = "utf8_iter" | ||
| 2759 | version = "1.0.4" | ||
| 2760 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2761 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" | ||
| 2762 | |||
| 2763 | [[package]] | ||
| 2764 | name = "uuid" | ||
| 2765 | version = "1.23.4" | ||
| 2766 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2767 | checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" | ||
| 2768 | dependencies = [ | ||
| 2769 | "getrandom 0.4.3", | ||
| 2770 | "js-sys", | ||
| 2771 | "wasm-bindgen", | ||
| 2772 | ] | ||
| 2773 | |||
| 2774 | [[package]] | ||
| 2775 | name = "valuable" | ||
| 2776 | version = "0.1.1" | ||
| 2777 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2778 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" | ||
| 2779 | |||
| 2780 | [[package]] | ||
| 2781 | name = "vcpkg" | ||
| 2782 | version = "0.2.15" | ||
| 2783 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2784 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" | ||
| 2785 | |||
| 2786 | [[package]] | ||
| 2787 | name = "version_check" | ||
| 2788 | version = "0.9.5" | ||
| 2789 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2790 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | ||
| 2791 | |||
| 2792 | [[package]] | ||
| 2793 | name = "walkdir" | ||
| 2794 | version = "2.5.0" | ||
| 2795 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2796 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" | ||
| 2797 | dependencies = [ | ||
| 2798 | "same-file", | ||
| 2799 | "winapi-util", | ||
| 2800 | ] | ||
| 2801 | |||
| 2802 | [[package]] | ||
| 2803 | name = "want" | ||
| 2804 | version = "0.3.1" | ||
| 2805 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2806 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" | ||
| 2807 | dependencies = [ | ||
| 2808 | "try-lock", | ||
| 2809 | ] | ||
| 2810 | |||
| 2811 | [[package]] | ||
| 2812 | name = "wasi" | ||
| 2813 | version = "0.11.1+wasi-snapshot-preview1" | ||
| 2814 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2815 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | ||
| 2816 | |||
| 2817 | [[package]] | ||
| 2818 | name = "wasm-bindgen" | ||
| 2819 | version = "0.2.126" | ||
| 2820 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2821 | checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" | ||
| 2822 | dependencies = [ | ||
| 2823 | "cfg-if", | ||
| 2824 | "once_cell", | ||
| 2825 | "rustversion", | ||
| 2826 | "wasm-bindgen-macro", | ||
| 2827 | "wasm-bindgen-shared", | ||
| 2828 | ] | ||
| 2829 | |||
| 2830 | [[package]] | ||
| 2831 | name = "wasm-bindgen-futures" | ||
| 2832 | version = "0.4.76" | ||
| 2833 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2834 | checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" | ||
| 2835 | dependencies = [ | ||
| 2836 | "js-sys", | ||
| 2837 | "wasm-bindgen", | ||
| 2838 | ] | ||
| 2839 | |||
| 2840 | [[package]] | ||
| 2841 | name = "wasm-bindgen-macro" | ||
| 2842 | version = "0.2.126" | ||
| 2843 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2844 | checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" | ||
| 2845 | dependencies = [ | ||
| 2846 | "quote", | ||
| 2847 | "wasm-bindgen-macro-support", | ||
| 2848 | ] | ||
| 2849 | |||
| 2850 | [[package]] | ||
| 2851 | name = "wasm-bindgen-macro-support" | ||
| 2852 | version = "0.2.126" | ||
| 2853 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2854 | checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" | ||
| 2855 | dependencies = [ | ||
| 2856 | "bumpalo", | ||
| 2857 | "proc-macro2", | ||
| 2858 | "quote", | ||
| 2859 | "syn", | ||
| 2860 | "wasm-bindgen-shared", | ||
| 2861 | ] | ||
| 2862 | |||
| 2863 | [[package]] | ||
| 2864 | name = "wasm-bindgen-shared" | ||
| 2865 | version = "0.2.126" | ||
| 2866 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2867 | checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" | ||
| 2868 | dependencies = [ | ||
| 2869 | "unicode-ident", | ||
| 2870 | ] | ||
| 2871 | |||
| 2872 | [[package]] | ||
| 2873 | name = "web-sys" | ||
| 2874 | version = "0.3.103" | ||
| 2875 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2876 | checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" | ||
| 2877 | dependencies = [ | ||
| 2878 | "js-sys", | ||
| 2879 | "wasm-bindgen", | ||
| 2880 | ] | ||
| 2881 | |||
| 2882 | [[package]] | ||
| 2883 | name = "web-time" | ||
| 2884 | version = "1.1.0" | ||
| 2885 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2886 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" | ||
| 2887 | dependencies = [ | ||
| 2888 | "js-sys", | ||
| 2889 | "wasm-bindgen", | ||
| 2890 | ] | ||
| 2891 | |||
| 2892 | [[package]] | ||
| 2893 | name = "webpki-root-certs" | ||
| 2894 | version = "1.0.8" | ||
| 2895 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2896 | checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" | ||
| 2897 | dependencies = [ | ||
| 2898 | "rustls-pki-types", | ||
| 2899 | ] | ||
| 2900 | |||
| 2901 | [[package]] | ||
| 2902 | name = "winapi" | ||
| 2903 | version = "0.3.9" | ||
| 2904 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2905 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" | ||
| 2906 | dependencies = [ | ||
| 2907 | "winapi-i686-pc-windows-gnu", | ||
| 2908 | "winapi-x86_64-pc-windows-gnu", | ||
| 2909 | ] | ||
| 2910 | |||
| 2911 | [[package]] | ||
| 2912 | name = "winapi-i686-pc-windows-gnu" | ||
| 2913 | version = "0.4.0" | ||
| 2914 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2915 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" | ||
| 2916 | |||
| 2917 | [[package]] | ||
| 2918 | name = "winapi-util" | ||
| 2919 | version = "0.1.11" | ||
| 2920 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2921 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" | ||
| 2922 | dependencies = [ | ||
| 2923 | "windows-sys 0.61.2", | ||
| 2924 | ] | ||
| 2925 | |||
| 2926 | [[package]] | ||
| 2927 | name = "winapi-x86_64-pc-windows-gnu" | ||
| 2928 | version = "0.4.0" | ||
| 2929 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2930 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" | ||
| 2931 | |||
| 2932 | [[package]] | ||
| 2933 | name = "windows-core" | ||
| 2934 | version = "0.62.2" | ||
| 2935 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2936 | checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" | ||
| 2937 | dependencies = [ | ||
| 2938 | "windows-implement", | ||
| 2939 | "windows-interface", | ||
| 2940 | "windows-link", | ||
| 2941 | "windows-result", | ||
| 2942 | "windows-strings", | ||
| 2943 | ] | ||
| 2944 | |||
| 2945 | [[package]] | ||
| 2946 | name = "windows-implement" | ||
| 2947 | version = "0.60.2" | ||
| 2948 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2949 | checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" | ||
| 2950 | dependencies = [ | ||
| 2951 | "proc-macro2", | ||
| 2952 | "quote", | ||
| 2953 | "syn", | ||
| 2954 | ] | ||
| 2955 | |||
| 2956 | [[package]] | ||
| 2957 | name = "windows-interface" | ||
| 2958 | version = "0.59.3" | ||
| 2959 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2960 | checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" | ||
| 2961 | dependencies = [ | ||
| 2962 | "proc-macro2", | ||
| 2963 | "quote", | ||
| 2964 | "syn", | ||
| 2965 | ] | ||
| 2966 | |||
| 2967 | [[package]] | ||
| 2968 | name = "windows-link" | ||
| 2969 | version = "0.2.1" | ||
| 2970 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2971 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | ||
| 2972 | |||
| 2973 | [[package]] | ||
| 2974 | name = "windows-registry" | ||
| 2975 | version = "0.6.1" | ||
| 2976 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2977 | checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" | ||
| 2978 | dependencies = [ | ||
| 2979 | "windows-link", | ||
| 2980 | "windows-result", | ||
| 2981 | "windows-strings", | ||
| 2982 | ] | ||
| 2983 | |||
| 2984 | [[package]] | ||
| 2985 | name = "windows-result" | ||
| 2986 | version = "0.4.1" | ||
| 2987 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2988 | checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" | ||
| 2989 | dependencies = [ | ||
| 2990 | "windows-link", | ||
| 2991 | ] | ||
| 2992 | |||
| 2993 | [[package]] | ||
| 2994 | name = "windows-strings" | ||
| 2995 | version = "0.5.1" | ||
| 2996 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2997 | checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" | ||
| 2998 | dependencies = [ | ||
| 2999 | "windows-link", | ||
| 3000 | ] | ||
| 3001 | |||
| 3002 | [[package]] | ||
| 3003 | name = "windows-sys" | ||
| 3004 | version = "0.52.0" | ||
| 3005 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3006 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" | ||
| 3007 | dependencies = [ | ||
| 3008 | "windows-targets", | ||
| 3009 | ] | ||
| 3010 | |||
| 3011 | [[package]] | ||
| 3012 | name = "windows-sys" | ||
| 3013 | version = "0.61.2" | ||
| 3014 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3015 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | ||
| 3016 | dependencies = [ | ||
| 3017 | "windows-link", | ||
| 3018 | ] | ||
| 3019 | |||
| 3020 | [[package]] | ||
| 3021 | name = "windows-targets" | ||
| 3022 | version = "0.52.6" | ||
| 3023 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3024 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" | ||
| 3025 | dependencies = [ | ||
| 3026 | "windows_aarch64_gnullvm", | ||
| 3027 | "windows_aarch64_msvc", | ||
| 3028 | "windows_i686_gnu", | ||
| 3029 | "windows_i686_gnullvm", | ||
| 3030 | "windows_i686_msvc", | ||
| 3031 | "windows_x86_64_gnu", | ||
| 3032 | "windows_x86_64_gnullvm", | ||
| 3033 | "windows_x86_64_msvc", | ||
| 3034 | ] | ||
| 3035 | |||
| 3036 | [[package]] | ||
| 3037 | name = "windows_aarch64_gnullvm" | ||
| 3038 | version = "0.52.6" | ||
| 3039 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3040 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" | ||
| 3041 | |||
| 3042 | [[package]] | ||
| 3043 | name = "windows_aarch64_msvc" | ||
| 3044 | version = "0.52.6" | ||
| 3045 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3046 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" | ||
| 3047 | |||
| 3048 | [[package]] | ||
| 3049 | name = "windows_i686_gnu" | ||
| 3050 | version = "0.52.6" | ||
| 3051 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3052 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" | ||
| 3053 | |||
| 3054 | [[package]] | ||
| 3055 | name = "windows_i686_gnullvm" | ||
| 3056 | version = "0.52.6" | ||
| 3057 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3058 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" | ||
| 3059 | |||
| 3060 | [[package]] | ||
| 3061 | name = "windows_i686_msvc" | ||
| 3062 | version = "0.52.6" | ||
| 3063 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3064 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" | ||
| 3065 | |||
| 3066 | [[package]] | ||
| 3067 | name = "windows_x86_64_gnu" | ||
| 3068 | version = "0.52.6" | ||
| 3069 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3070 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" | ||
| 3071 | |||
| 3072 | [[package]] | ||
| 3073 | name = "windows_x86_64_gnullvm" | ||
| 3074 | version = "0.52.6" | ||
| 3075 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3076 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" | ||
| 3077 | |||
| 3078 | [[package]] | ||
| 3079 | name = "windows_x86_64_msvc" | ||
| 3080 | version = "0.52.6" | ||
| 3081 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3082 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | ||
| 3083 | |||
| 3084 | [[package]] | ||
| 3085 | name = "winnow" | ||
| 3086 | version = "1.0.3" | ||
| 3087 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3088 | checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" | ||
| 3089 | dependencies = [ | ||
| 3090 | "memchr", | ||
| 3091 | ] | ||
| 3092 | |||
| 3093 | [[package]] | ||
| 3094 | name = "writeable" | ||
| 3095 | version = "0.6.3" | ||
| 3096 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3097 | checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" | ||
| 3098 | |||
| 3099 | [[package]] | ||
| 3100 | name = "yoke" | ||
| 3101 | version = "0.8.3" | ||
| 3102 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3103 | checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" | ||
| 3104 | dependencies = [ | ||
| 3105 | "stable_deref_trait", | ||
| 3106 | "yoke-derive", | ||
| 3107 | "zerofrom", | ||
| 3108 | ] | ||
| 3109 | |||
| 3110 | [[package]] | ||
| 3111 | name = "yoke-derive" | ||
| 3112 | version = "0.8.2" | ||
| 3113 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3114 | checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" | ||
| 3115 | dependencies = [ | ||
| 3116 | "proc-macro2", | ||
| 3117 | "quote", | ||
| 3118 | "syn", | ||
| 3119 | "synstructure", | ||
| 3120 | ] | ||
| 3121 | |||
| 3122 | [[package]] | ||
| 3123 | name = "zerocopy" | ||
| 3124 | version = "0.8.54" | ||
| 3125 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3126 | checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" | ||
| 3127 | dependencies = [ | ||
| 3128 | "zerocopy-derive", | ||
| 3129 | ] | ||
| 3130 | |||
| 3131 | [[package]] | ||
| 3132 | name = "zerocopy-derive" | ||
| 3133 | version = "0.8.54" | ||
| 3134 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3135 | checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" | ||
| 3136 | dependencies = [ | ||
| 3137 | "proc-macro2", | ||
| 3138 | "quote", | ||
| 3139 | "syn", | ||
| 3140 | ] | ||
| 3141 | |||
| 3142 | [[package]] | ||
| 3143 | name = "zerofrom" | ||
| 3144 | version = "0.1.8" | ||
| 3145 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3146 | checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" | ||
| 3147 | dependencies = [ | ||
| 3148 | "zerofrom-derive", | ||
| 3149 | ] | ||
| 3150 | |||
| 3151 | [[package]] | ||
| 3152 | name = "zerofrom-derive" | ||
| 3153 | version = "0.1.7" | ||
| 3154 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3155 | checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" | ||
| 3156 | dependencies = [ | ||
| 3157 | "proc-macro2", | ||
| 3158 | "quote", | ||
| 3159 | "syn", | ||
| 3160 | "synstructure", | ||
| 3161 | ] | ||
| 3162 | |||
| 3163 | [[package]] | ||
| 3164 | name = "zeroize" | ||
| 3165 | version = "1.9.0" | ||
| 3166 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3167 | checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" | ||
| 3168 | |||
| 3169 | [[package]] | ||
| 3170 | name = "zerotrie" | ||
| 3171 | version = "0.2.4" | ||
| 3172 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3173 | checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" | ||
| 3174 | dependencies = [ | ||
| 3175 | "displaydoc", | ||
| 3176 | "yoke", | ||
| 3177 | "zerofrom", | ||
| 3178 | ] | ||
| 3179 | |||
| 3180 | [[package]] | ||
| 3181 | name = "zerovec" | ||
| 3182 | version = "0.11.6" | ||
| 3183 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3184 | checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" | ||
| 3185 | dependencies = [ | ||
| 3186 | "yoke", | ||
| 3187 | "zerofrom", | ||
| 3188 | "zerovec-derive", | ||
| 3189 | ] | ||
| 3190 | |||
| 3191 | [[package]] | ||
| 3192 | name = "zerovec-derive" | ||
| 3193 | version = "0.11.3" | ||
| 3194 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3195 | checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" | ||
| 3196 | dependencies = [ | ||
| 3197 | "proc-macro2", | ||
| 3198 | "quote", | ||
| 3199 | "syn", | ||
| 3200 | ] | ||
| 3201 | |||
| 3202 | [[package]] | ||
| 3203 | name = "zip" | ||
| 3204 | version = "8.6.0" | ||
| 3205 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3206 | checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" | ||
| 3207 | dependencies = [ | ||
| 3208 | "aes 0.9.1", | ||
| 3209 | "bzip2", | ||
| 3210 | "constant_time_eq", | ||
| 3211 | "crc32fast", | ||
| 3212 | "deflate64", | ||
| 3213 | "flate2", | ||
| 3214 | "getrandom 0.4.3", | ||
| 3215 | "hmac", | ||
| 3216 | "indexmap 2.14.0", | ||
| 3217 | "lzma-rust2", | ||
| 3218 | "memchr", | ||
| 3219 | "pbkdf2", | ||
| 3220 | "ppmd-rust", | ||
| 3221 | "sha1", | ||
| 3222 | "time", | ||
| 3223 | "typed-path", | ||
| 3224 | "zeroize", | ||
| 3225 | "zopfli", | ||
| 3226 | "zstd", | ||
| 3227 | ] | ||
| 3228 | |||
| 3229 | [[package]] | ||
| 3230 | name = "zlib-rs" | ||
| 3231 | version = "0.6.6" | ||
| 3232 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3233 | checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" | ||
| 3234 | |||
| 3235 | [[package]] | ||
| 3236 | name = "zmij" | ||
| 3237 | version = "1.0.21" | ||
| 3238 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3239 | checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" | ||
| 3240 | |||
| 3241 | [[package]] | ||
| 3242 | name = "zopfli" | ||
| 3243 | version = "0.8.3" | ||
| 3244 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3245 | checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" | ||
| 3246 | dependencies = [ | ||
| 3247 | "bumpalo", | ||
| 3248 | "crc32fast", | ||
| 3249 | "log", | ||
| 3250 | "simd-adler32", | ||
| 3251 | ] | ||
| 3252 | |||
| 3253 | [[package]] | ||
| 3254 | name = "zstd" | ||
| 3255 | version = "0.13.3" | ||
| 3256 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3257 | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" | ||
| 3258 | dependencies = [ | ||
| 3259 | "zstd-safe", | ||
| 3260 | ] | ||
| 3261 | |||
| 3262 | [[package]] | ||
| 3263 | name = "zstd-safe" | ||
| 3264 | version = "7.2.4" | ||
| 3265 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3266 | checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" | ||
| 3267 | dependencies = [ | ||
| 3268 | "zstd-sys", | ||
| 3269 | ] | ||
| 3270 | |||
| 3271 | [[package]] | ||
| 3272 | name = "zstd-sys" | ||
| 3273 | version = "2.0.16+zstd.1.5.7" | ||
| 3274 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3275 | checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" | ||
| 3276 | dependencies = [ | ||
| 3277 | "cc", | ||
| 3278 | "pkg-config", | ||
| 3279 | ] | ||
| @@ -4,3 +4,26 @@ version = "0.1.0" | |||
| 4 | edition = "2024" | 4 | edition = "2024" |
| 5 | 5 | ||
| 6 | [dependencies] | 6 | [dependencies] |
| 7 | axum = { version = "0.8.9", features = ["macros"] } | ||
| 8 | tokio = { version = "1.52.3", features = ["full"] } | ||
| 9 | askama = "0.16" | ||
| 10 | rusqlite = { version = "0.40.1", features = ["bundled"] } | ||
| 11 | bollard = "0.17" | ||
| 12 | serde = { version = "1.0.228", features = ["derive"] } | ||
| 13 | serde_json = "1.0.150" | ||
| 14 | toml = "1.1.2" | ||
| 15 | tower-http = { version = "0.7", features = ["fs"] } | ||
| 16 | tower = "0.5.3" | ||
| 17 | argon2 = "0.5.3" | ||
| 18 | uuid = { version = "1.23.4", features = ["v4"] } | ||
| 19 | rand = "0.8" | ||
| 20 | zip = "8.6.0" | ||
| 21 | tempfile = "3.27" | ||
| 22 | tracing = "0.1.44" | ||
| 23 | tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } | ||
| 24 | reqwest = { version = "0.13.4", features = ["json"] } | ||
| 25 | |||
| 26 | chrono = { version = "0.4.45", features = ["serde"] } | ||
| 27 | cookie = "0.18.1" | ||
| 28 | axum-extra = { version = "0.12.6", features = ["cookie-private"] } | ||
| 29 | futures-util = "0.3.32" | ||
diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..49fa85a --- /dev/null +++ b/config.toml | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | # Minecraft UI Configuration | ||
| 2 | # All fields are required on first run. | ||
| 3 | |||
| 4 | # Path to the Server Directory where Minecraft data lives. | ||
| 5 | # Relative paths are resolved from the working directory. | ||
| 6 | server_directory = "./minecraft" | ||
| 7 | |||
| 8 | # Port the webUI listens on (nginx will proxy to this). | ||
| 9 | webui_port = 3000 | ||
| 10 | |||
| 11 | # Maximum memory in MB that any single Instance can allocate. | ||
| 12 | max_memory_mb = 20480 | ||
| 13 | |||
| 14 | # Initial superuser credentials (only used on first run to bootstrap). | ||
| 15 | # The superuser MUST change their password on first login. | ||
| 16 | initial_superuser = "admin" | ||
| 17 | initial_superuser_password = "admin" | ||
diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..8fa62ec --- /dev/null +++ b/src/auth.rs | |||
| @@ -0,0 +1,194 @@ | |||
| 1 | use argon2::{ | ||
| 2 | Argon2, PasswordHash, PasswordHasher, PasswordVerifier, | ||
| 3 | password_hash::{SaltString, rand_core::OsRng}, | ||
| 4 | }; | ||
| 5 | use axum::{ | ||
| 6 | extract::{FromRequestParts, Request, State}, | ||
| 7 | http::{StatusCode, request::Parts}, | ||
| 8 | middleware::Next, | ||
| 9 | response::{IntoResponse, Redirect, Response}, | ||
| 10 | }; | ||
| 11 | use rusqlite::params; | ||
| 12 | use serde::Deserialize; | ||
| 13 | use uuid::Uuid; | ||
| 14 | |||
| 15 | use crate::models::Account; | ||
| 16 | use crate::routes::AppState; | ||
| 17 | |||
| 18 | #[derive(Debug, Deserialize)] | ||
| 19 | pub struct LoginForm { | ||
| 20 | pub username: String, | ||
| 21 | pub password: String, | ||
| 22 | } | ||
| 23 | |||
| 24 | const SESSION_COOKIE: &str = "mcui_session"; | ||
| 25 | |||
| 26 | pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> { | ||
| 27 | let salt = SaltString::generate(&mut OsRng); | ||
| 28 | let hash = Argon2::default().hash_password(password.as_bytes(), &salt)?; | ||
| 29 | Ok(hash.to_string()) | ||
| 30 | } | ||
| 31 | |||
| 32 | pub fn verify_password(password: &str, hash: &str) -> Result<bool, argon2::password_hash::Error> { | ||
| 33 | let parsed = PasswordHash::new(hash)?; | ||
| 34 | Ok(Argon2::default() | ||
| 35 | .verify_password(password.as_bytes(), &parsed) | ||
| 36 | .is_ok()) | ||
| 37 | } | ||
| 38 | |||
| 39 | pub async fn authenticate( | ||
| 40 | State(state): State<AppState>, | ||
| 41 | axum::Form(form): axum::Form<LoginForm>, | ||
| 42 | ) -> Result<impl IntoResponse, (StatusCode, String)> { | ||
| 43 | let conn = state.db.conn(); | ||
| 44 | |||
| 45 | let account = conn | ||
| 46 | .query_row( | ||
| 47 | "SELECT id, username, is_superuser, password_change_required FROM accounts WHERE username = ?1", | ||
| 48 | params![form.username], | ||
| 49 | |row| { | ||
| 50 | Ok(Account { | ||
| 51 | id: row.get(0)?, | ||
| 52 | username: row.get(1)?, | ||
| 53 | is_superuser: row.get::<_, i32>(2)? != 0, | ||
| 54 | password_change_required: row.get::<_, i32>(3)? != 0, | ||
| 55 | }) | ||
| 56 | }, | ||
| 57 | ) | ||
| 58 | .map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid username or password".into()))?; | ||
| 59 | |||
| 60 | let hash: String = conn | ||
| 61 | .query_row( | ||
| 62 | "SELECT password FROM accounts WHERE id = ?1", | ||
| 63 | params![account.id], | ||
| 64 | |row| row.get(0), | ||
| 65 | ) | ||
| 66 | .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error".into()))?; | ||
| 67 | |||
| 68 | if !verify_password(&form.password, &hash).unwrap_or(false) { | ||
| 69 | return Err(( | ||
| 70 | StatusCode::UNAUTHORIZED, | ||
| 71 | "Invalid username or password".into(), | ||
| 72 | )); | ||
| 73 | } | ||
| 74 | |||
| 75 | let session_id = Uuid::new_v4().to_string(); | ||
| 76 | conn.execute( | ||
| 77 | "INSERT INTO sessions (id, account_id) VALUES (?1, ?2)", | ||
| 78 | params![session_id, account.id], | ||
| 79 | ) | ||
| 80 | .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Session error".into()))?; | ||
| 81 | drop(conn); | ||
| 82 | |||
| 83 | let cookie_value = | ||
| 84 | format!("{SESSION_COOKIE}={session_id}; Path=/; HttpOnly; Max-Age=86400; SameSite=Lax"); | ||
| 85 | |||
| 86 | let redirect = if account.password_change_required { | ||
| 87 | Redirect::to("/change-password") | ||
| 88 | } else { | ||
| 89 | Redirect::to("/") | ||
| 90 | }; | ||
| 91 | |||
| 92 | Ok(([(axum::http::header::SET_COOKIE, cookie_value)], redirect)) | ||
| 93 | } | ||
| 94 | |||
| 95 | pub async fn require_auth( | ||
| 96 | State(state): State<AppState>, | ||
| 97 | mut request: Request, | ||
| 98 | next: Next, | ||
| 99 | ) -> Result<Response, Response> { | ||
| 100 | let cookies = request | ||
| 101 | .headers() | ||
| 102 | .get(axum::http::header::COOKIE) | ||
| 103 | .and_then(|v| v.to_str().ok()) | ||
| 104 | .unwrap_or(""); | ||
| 105 | |||
| 106 | let session_id = cookies.split(';').map(|s| s.trim()).find_map(|cookie| { | ||
| 107 | if let Some((name, value)) = cookie.split_once('=') { | ||
| 108 | if name.trim() == SESSION_COOKIE { | ||
| 109 | Some(value.trim().to_string()) | ||
| 110 | } else { | ||
| 111 | None | ||
| 112 | } | ||
| 113 | } else { | ||
| 114 | None | ||
| 115 | } | ||
| 116 | }); | ||
| 117 | |||
| 118 | match session_id { | ||
| 119 | Some(id) => { | ||
| 120 | let account = state | ||
| 121 | .db | ||
| 122 | .conn() | ||
| 123 | .query_row( | ||
| 124 | "SELECT a.id, a.username, a.is_superuser, a.password_change_required | ||
| 125 | FROM sessions s JOIN accounts a ON s.account_id = a.id | ||
| 126 | WHERE s.id = ?1", | ||
| 127 | params![id], | ||
| 128 | |row| { | ||
| 129 | Ok(Account { | ||
| 130 | id: row.get(0)?, | ||
| 131 | username: row.get(1)?, | ||
| 132 | is_superuser: row.get::<_, i32>(2)? != 0, | ||
| 133 | password_change_required: row.get::<_, i32>(3)? != 0, | ||
| 134 | }) | ||
| 135 | }, | ||
| 136 | ) | ||
| 137 | .ok(); | ||
| 138 | |||
| 139 | if let Some(account) = account { | ||
| 140 | request.extensions_mut().insert(account); | ||
| 141 | Ok(next.run(request).await) | ||
| 142 | } else { | ||
| 143 | Err(Redirect::to("/login").into_response()) | ||
| 144 | } | ||
| 145 | } | ||
| 146 | None => Err(Redirect::to("/login").into_response()), | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | #[derive(Clone)] | ||
| 151 | pub struct CurrentUser(pub Account); | ||
| 152 | |||
| 153 | impl<S> FromRequestParts<S> for CurrentUser | ||
| 154 | where | ||
| 155 | S: Send + Sync, | ||
| 156 | { | ||
| 157 | type Rejection = (StatusCode, &'static str); | ||
| 158 | |||
| 159 | async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { | ||
| 160 | parts | ||
| 161 | .extensions | ||
| 162 | .get::<Account>() | ||
| 163 | .cloned() | ||
| 164 | .map(CurrentUser) | ||
| 165 | .ok_or((StatusCode::UNAUTHORIZED, "Not authenticated")) | ||
| 166 | } | ||
| 167 | } | ||
| 168 | |||
| 169 | pub async fn logout( | ||
| 170 | State(state): State<AppState>, | ||
| 171 | headers: axum::http::HeaderMap, | ||
| 172 | ) -> impl IntoResponse { | ||
| 173 | if let Some(cookie_header) = headers.get(axum::http::header::COOKIE) { | ||
| 174 | if let Ok(cookie_str) = cookie_header.to_str() { | ||
| 175 | for part in cookie_str.split(';') { | ||
| 176 | let part = part.trim(); | ||
| 177 | if let Some((name, value)) = part.split_once('=') { | ||
| 178 | if name.trim() == SESSION_COOKIE { | ||
| 179 | let _ = state | ||
| 180 | .db | ||
| 181 | .conn() | ||
| 182 | .execute("DELETE FROM sessions WHERE id = ?1", params![value.trim()]); | ||
| 183 | } | ||
| 184 | } | ||
| 185 | } | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | let clear_cookie = format!("{SESSION_COOKIE}=; Path=/; HttpOnly; Max-Age=0"); | ||
| 190 | ( | ||
| 191 | [(axum::http::header::SET_COOKIE, clear_cookie)], | ||
| 192 | Redirect::to("/login"), | ||
| 193 | ) | ||
| 194 | } | ||
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d9f6103 --- /dev/null +++ b/src/config.rs | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | use serde::Deserialize; | ||
| 2 | use std::path::PathBuf; | ||
| 3 | |||
| 4 | #[derive(Debug, Clone, Deserialize)] | ||
| 5 | pub struct Config { | ||
| 6 | pub server_directory: PathBuf, | ||
| 7 | pub webui_port: u16, | ||
| 8 | pub max_memory_mb: u32, | ||
| 9 | pub initial_superuser: String, | ||
| 10 | pub initial_superuser_password: String, | ||
| 11 | } | ||
| 12 | |||
| 13 | impl Config { | ||
| 14 | pub fn load() -> Result<Self, String> { | ||
| 15 | let content = std::fs::read_to_string("config.toml") | ||
| 16 | .map_err(|e| format!("Failed to read config.toml: {e}"))?; | ||
| 17 | |||
| 18 | let config: Config = | ||
| 19 | toml::from_str(&content).map_err(|e| format!("Failed to parse config.toml: {e}"))?; | ||
| 20 | |||
| 21 | Ok(config) | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..7868e3e --- /dev/null +++ b/src/db.rs | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | use rusqlite::Connection; | ||
| 2 | use std::path::Path; | ||
| 3 | use std::sync::{Arc, Mutex}; | ||
| 4 | |||
| 5 | #[derive(Clone)] | ||
| 6 | pub struct Database { | ||
| 7 | conn: Arc<Mutex<Connection>>, | ||
| 8 | } | ||
| 9 | |||
| 10 | // Safe because all access to Connection goes through Mutex::lock() | ||
| 11 | unsafe impl Sync for Database {} | ||
| 12 | |||
| 13 | impl Database { | ||
| 14 | pub fn open(path: &Path) -> Result<Self, rusqlite::Error> { | ||
| 15 | let conn = Connection::open(path)?; | ||
| 16 | conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?; | ||
| 17 | let db = Self { | ||
| 18 | conn: Arc::new(Mutex::new(conn)), | ||
| 19 | }; | ||
| 20 | db.migrate()?; | ||
| 21 | Ok(db) | ||
| 22 | } | ||
| 23 | |||
| 24 | fn migrate(&self) -> Result<(), rusqlite::Error> { | ||
| 25 | let conn = self.conn.lock().unwrap(); | ||
| 26 | conn.execute_batch( | ||
| 27 | " | ||
| 28 | CREATE TABLE IF NOT EXISTS accounts ( | ||
| 29 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 30 | username TEXT NOT NULL UNIQUE, | ||
| 31 | password TEXT NOT NULL, | ||
| 32 | is_superuser INTEGER NOT NULL DEFAULT 0, | ||
| 33 | password_change_required INTEGER NOT NULL DEFAULT 0 | ||
| 34 | ); | ||
| 35 | |||
| 36 | CREATE TABLE IF NOT EXISTS sessions ( | ||
| 37 | id TEXT PRIMARY KEY, | ||
| 38 | account_id INTEGER NOT NULL, | ||
| 39 | created_at TEXT NOT NULL DEFAULT (datetime('now')), | ||
| 40 | FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE | ||
| 41 | ); | ||
| 42 | |||
| 43 | CREATE TABLE IF NOT EXISTS instances ( | ||
| 44 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 45 | name TEXT NOT NULL UNIQUE, | ||
| 46 | version TEXT NOT NULL, | ||
| 47 | server_type TEXT NOT NULL CHECK(server_type IN ('VANILLA', 'FORGE', 'FABRIC')), | ||
| 48 | world_source TEXT NOT NULL CHECK(world_source IN ('generate', 'upload')), | ||
| 49 | world_seed TEXT, | ||
| 50 | level_type TEXT NOT NULL DEFAULT 'DEFAULT', | ||
| 51 | has_modpack INTEGER NOT NULL DEFAULT 0, | ||
| 52 | memory_mb INTEGER NOT NULL, | ||
| 53 | difficulty INTEGER NOT NULL DEFAULT 1, | ||
| 54 | gamemode INTEGER NOT NULL DEFAULT 0, | ||
| 55 | max_players INTEGER NOT NULL DEFAULT 20, | ||
| 56 | pvp INTEGER NOT NULL DEFAULT 1, | ||
| 57 | motd TEXT NOT NULL DEFAULT 'A Minecraft Server', | ||
| 58 | rcon_password TEXT NOT NULL, | ||
| 59 | is_active INTEGER NOT NULL DEFAULT 0, | ||
| 60 | container_state TEXT NOT NULL DEFAULT 'stopped', | ||
| 61 | created_at TEXT NOT NULL DEFAULT (datetime('now')), | ||
| 62 | updated_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| 63 | ); | ||
| 64 | |||
| 65 | CREATE TABLE IF NOT EXISTS backups ( | ||
| 66 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 67 | instance_id INTEGER NOT NULL, | ||
| 68 | filename TEXT NOT NULL, | ||
| 69 | created_at TEXT NOT NULL DEFAULT (datetime('now')), | ||
| 70 | FOREIGN KEY (instance_id) REFERENCES instances(id) ON DELETE CASCADE | ||
| 71 | ); | ||
| 72 | |||
| 73 | CREATE TABLE IF NOT EXISTS whitelist ( | ||
| 74 | instance_id INTEGER NOT NULL, | ||
| 75 | username TEXT NOT NULL, | ||
| 76 | PRIMARY KEY (instance_id, username), | ||
| 77 | FOREIGN KEY (instance_id) REFERENCES instances(id) ON DELETE CASCADE | ||
| 78 | ); | ||
| 79 | |||
| 80 | CREATE TABLE IF NOT EXISTS ops ( | ||
| 81 | instance_id INTEGER NOT NULL, | ||
| 82 | username TEXT NOT NULL, | ||
| 83 | PRIMARY KEY (instance_id, username), | ||
| 84 | FOREIGN KEY (instance_id) REFERENCES instances(id) ON DELETE CASCADE | ||
| 85 | ); | ||
| 86 | |||
| 87 | CREATE TABLE IF NOT EXISTS bans ( | ||
| 88 | instance_id INTEGER NOT NULL, | ||
| 89 | username TEXT NOT NULL, | ||
| 90 | ban_type TEXT NOT NULL CHECK(ban_type IN ('user', 'ip')), | ||
| 91 | PRIMARY KEY (instance_id, username), | ||
| 92 | FOREIGN KEY (instance_id) REFERENCES instances(id) ON DELETE CASCADE | ||
| 93 | ); | ||
| 94 | ", | ||
| 95 | )?; | ||
| 96 | |||
| 97 | // Migration: add container_state column if missing (for older DBs) | ||
| 98 | let _ = conn.execute_batch( | ||
| 99 | "ALTER TABLE instances ADD COLUMN container_state TEXT NOT NULL DEFAULT 'stopped'", | ||
| 100 | ); | ||
| 101 | Ok(()) | ||
| 102 | } | ||
| 103 | |||
| 104 | pub fn conn(&self) -> std::sync::MutexGuard<'_, Connection> { | ||
| 105 | self.conn.lock().unwrap() | ||
| 106 | } | ||
| 107 | } | ||
diff --git a/src/docker.rs b/src/docker.rs new file mode 100644 index 0000000..b18bb47 --- /dev/null +++ b/src/docker.rs | |||
| @@ -0,0 +1,213 @@ | |||
| 1 | use bollard::Docker; | ||
| 2 | use bollard::container::{ | ||
| 3 | Config as ContainerConfig, CreateContainerOptions, ListContainersOptions, | ||
| 4 | RemoveContainerOptions, StartContainerOptions, StopContainerOptions, | ||
| 5 | }; | ||
| 6 | use bollard::image::CreateImageOptions; | ||
| 7 | use bollard::models::HostConfig; | ||
| 8 | use futures_util::StreamExt; | ||
| 9 | use std::collections::HashMap; | ||
| 10 | |||
| 11 | use crate::config::Config; | ||
| 12 | use crate::models::Instance; | ||
| 13 | |||
| 14 | #[derive(Clone)] | ||
| 15 | pub struct DockerManager { | ||
| 16 | client: Docker, | ||
| 17 | config: Config, | ||
| 18 | } | ||
| 19 | |||
| 20 | #[derive(Debug, Clone)] | ||
| 21 | pub enum ContainerStatus { | ||
| 22 | Running, | ||
| 23 | Stopped, | ||
| 24 | Starting, | ||
| 25 | NotFound, | ||
| 26 | Error(String), | ||
| 27 | } | ||
| 28 | |||
| 29 | impl DockerManager { | ||
| 30 | pub fn new(config: Config) -> Result<Self, bollard::errors::Error> { | ||
| 31 | let client = Docker::connect_with_local_defaults()?; | ||
| 32 | Ok(Self { client, config }) | ||
| 33 | } | ||
| 34 | |||
| 35 | pub async fn container_status(&self) -> ContainerStatus { | ||
| 36 | let mut filters = HashMap::new(); | ||
| 37 | filters.insert("name".to_string(), vec!["mc".to_string()]); | ||
| 38 | |||
| 39 | let options = ListContainersOptions { | ||
| 40 | all: true, | ||
| 41 | filters, | ||
| 42 | ..Default::default() | ||
| 43 | }; | ||
| 44 | |||
| 45 | match self.client.list_containers(Some(options)).await { | ||
| 46 | Ok(containers) => { | ||
| 47 | if containers.is_empty() { | ||
| 48 | ContainerStatus::NotFound | ||
| 49 | } else { | ||
| 50 | match containers[0].state.as_deref() { | ||
| 51 | Some("running") => ContainerStatus::Running, | ||
| 52 | Some("exited") | Some("stopped") => ContainerStatus::Stopped, | ||
| 53 | _ => ContainerStatus::Starting, | ||
| 54 | } | ||
| 55 | } | ||
| 56 | } | ||
| 57 | Err(e) => ContainerStatus::Error(e.to_string()), | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | pub async fn start_container(&self, instance: &Instance) -> Result<(), String> { | ||
| 62 | tracing::info!("Pulling itzg/minecraft-server:latest image..."); | ||
| 63 | self.pull_image().await?; | ||
| 64 | tracing::info!("Creating container for instance '{}'", instance.name); | ||
| 65 | self.recreate_container(instance).await?; | ||
| 66 | tracing::info!("Starting container..."); | ||
| 67 | self.start_existing().await | ||
| 68 | } | ||
| 69 | |||
| 70 | async fn pull_image(&self) -> Result<(), String> { | ||
| 71 | let options = CreateImageOptions { | ||
| 72 | from_image: "itzg/minecraft-server", | ||
| 73 | tag: "latest", | ||
| 74 | ..Default::default() | ||
| 75 | }; | ||
| 76 | |||
| 77 | let mut stream = self.client.create_image(Some(options), None, None); | ||
| 78 | |||
| 79 | while let Some(result) = stream.next().await { | ||
| 80 | match result { | ||
| 81 | Ok(_info) => {} | ||
| 82 | Err(e) => return Err(format!("Image pull failed: {e}")), | ||
| 83 | } | ||
| 84 | } | ||
| 85 | Ok(()) | ||
| 86 | } | ||
| 87 | |||
| 88 | async fn recreate_container(&self, instance: &Instance) -> Result<(), String> { | ||
| 89 | // Remove existing container if present | ||
| 90 | let remove_options = RemoveContainerOptions { | ||
| 91 | force: true, | ||
| 92 | ..Default::default() | ||
| 93 | }; | ||
| 94 | match self | ||
| 95 | .client | ||
| 96 | .remove_container("mc", Some(remove_options)) | ||
| 97 | .await | ||
| 98 | { | ||
| 99 | Ok(_) => tracing::info!("Removed existing container"), | ||
| 100 | Err(e) => tracing::debug!("No existing container to remove: {e}"), | ||
| 101 | } | ||
| 102 | |||
| 103 | let server_dir = self | ||
| 104 | .config | ||
| 105 | .server_directory | ||
| 106 | .canonicalize() | ||
| 107 | .unwrap_or_else(|_| self.config.server_directory.clone()) | ||
| 108 | .display() | ||
| 109 | .to_string(); | ||
| 110 | let instance_data = format!("{server_dir}/instances/{}/data", instance.name); | ||
| 111 | |||
| 112 | let mut env_strings: Vec<String> = vec![ | ||
| 113 | "EULA=TRUE".into(), | ||
| 114 | format!("VERSION={}", instance.version), | ||
| 115 | format!("TYPE={}", instance.server_type), | ||
| 116 | format!("MEMORY={}M", instance.memory_mb), | ||
| 117 | format!("MAX_PLAYERS={}", instance.max_players), | ||
| 118 | format!("DIFFICULTY={}", instance.difficulty), | ||
| 119 | "FORCE_GAMEMODE=true".into(), | ||
| 120 | "SPAWN_PROTECTION=0".into(), | ||
| 121 | "ENABLE_WHITELIST=true".into(), | ||
| 122 | "PAUSE_WHEN_EMPTY_SECONDS=30".into(), | ||
| 123 | "ONLINE_MODE=false".into(), | ||
| 124 | "TZ=Europe/Warsaw".into(), | ||
| 125 | "USE_AIKAR_FLAGS=true".into(), | ||
| 126 | "ENABLE_RCON=true".into(), | ||
| 127 | format!("RCON_PASSWORD={}", instance.rcon_password), | ||
| 128 | "RCON_PORT=25575".into(), | ||
| 129 | format!("MOTD={}", instance.motd), | ||
| 130 | format!("GAMEMODE={}", instance.gamemode), | ||
| 131 | format!("LEVEL_TYPE={}", instance.level_type), | ||
| 132 | ]; | ||
| 133 | |||
| 134 | if instance.pvp { | ||
| 135 | env_strings.push("PVP=true".into()); | ||
| 136 | } else { | ||
| 137 | env_strings.push("PVP=false".into()); | ||
| 138 | } | ||
| 139 | |||
| 140 | if let Some(ref seed) = instance.world_seed { | ||
| 141 | if !seed.is_empty() { | ||
| 142 | env_strings.push(format!("LEVEL_SEED={seed}")); | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | let env_refs: Vec<&str> = env_strings.iter().map(|s| s.as_str()).collect(); | ||
| 147 | |||
| 148 | let mut exposed_ports: HashMap<&str, HashMap<(), ()>> = HashMap::new(); | ||
| 149 | exposed_ports.insert("25565/tcp", HashMap::new()); | ||
| 150 | exposed_ports.insert("25575/tcp", HashMap::new()); | ||
| 151 | |||
| 152 | let mut port_bindings = HashMap::new(); | ||
| 153 | port_bindings.insert( | ||
| 154 | "25565/tcp".to_string(), | ||
| 155 | Some(vec![bollard::models::PortBinding { | ||
| 156 | host_ip: None, | ||
| 157 | host_port: Some("25565".to_string()), | ||
| 158 | }]), | ||
| 159 | ); | ||
| 160 | port_bindings.insert( | ||
| 161 | "25575/tcp".to_string(), | ||
| 162 | Some(vec![bollard::models::PortBinding { | ||
| 163 | host_ip: None, | ||
| 164 | host_port: Some("25575".to_string()), | ||
| 165 | }]), | ||
| 166 | ); | ||
| 167 | |||
| 168 | let host_config = HostConfig { | ||
| 169 | binds: Some(vec![format!("{instance_data}:/data")]), | ||
| 170 | port_bindings: Some(port_bindings), | ||
| 171 | ..Default::default() | ||
| 172 | }; | ||
| 173 | |||
| 174 | let container_config = ContainerConfig { | ||
| 175 | image: Some("itzg/minecraft-server:latest"), | ||
| 176 | tty: Some(true), | ||
| 177 | env: Some(env_refs), | ||
| 178 | exposed_ports: Some(exposed_ports), | ||
| 179 | host_config: Some(host_config), | ||
| 180 | ..Default::default() | ||
| 181 | }; | ||
| 182 | |||
| 183 | let options = CreateContainerOptions { | ||
| 184 | name: "mc", | ||
| 185 | ..Default::default() | ||
| 186 | }; | ||
| 187 | |||
| 188 | self.client | ||
| 189 | .create_container(Some(options), container_config) | ||
| 190 | .await | ||
| 191 | .map_err(|e| format!("Failed to create container: {e}"))?; | ||
| 192 | |||
| 193 | tracing::info!("Container 'mc' created successfully"); | ||
| 194 | Ok(()) | ||
| 195 | } | ||
| 196 | |||
| 197 | async fn start_existing(&self) -> Result<(), String> { | ||
| 198 | self.client | ||
| 199 | .start_container("mc", None::<StartContainerOptions<&str>>) | ||
| 200 | .await | ||
| 201 | .map_err(|e| format!("Failed to start container: {e}"))?; | ||
| 202 | Ok(()) | ||
| 203 | } | ||
| 204 | |||
| 205 | pub async fn stop_container(&self) -> Result<(), String> { | ||
| 206 | let options = StopContainerOptions { t: 30 }; | ||
| 207 | self.client | ||
| 208 | .stop_container("mc", Some(options)) | ||
| 209 | .await | ||
| 210 | .map_err(|e| format!("Failed to stop container: {e}"))?; | ||
| 211 | Ok(()) | ||
| 212 | } | ||
| 213 | } | ||
diff --git a/src/main.rs b/src/main.rs index e7a11a9..a96b41b 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -1,3 +1,98 @@ | |||
| 1 | fn main() { | 1 | mod auth; |
| 2 | println!("Hello, world!"); | 2 | mod config; |
| 3 | mod db; | ||
| 4 | mod docker; | ||
| 5 | mod models; | ||
| 6 | mod rcon; | ||
| 7 | mod routes; | ||
| 8 | |||
| 9 | use tracing_subscriber::EnvFilter; | ||
| 10 | |||
| 11 | use crate::db::Database; | ||
| 12 | use crate::docker::DockerManager; | ||
| 13 | use crate::routes::AppState; | ||
| 14 | |||
| 15 | #[tokio::main] | ||
| 16 | async fn main() { | ||
| 17 | tracing_subscriber::fmt() | ||
| 18 | .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) | ||
| 19 | .init(); | ||
| 20 | |||
| 21 | let config = config::Config::load().unwrap_or_else(|e| { | ||
| 22 | eprintln!("Configuration error: {e}"); | ||
| 23 | eprintln!("Please create a config.toml file based on the example."); | ||
| 24 | std::process::exit(1); | ||
| 25 | }); | ||
| 26 | |||
| 27 | if !config.server_directory.exists() { | ||
| 28 | eprintln!( | ||
| 29 | "Server directory does not exist: {}", | ||
| 30 | config.server_directory.display() | ||
| 31 | ); | ||
| 32 | eprintln!("Please create it or update config.toml."); | ||
| 33 | std::process::exit(1); | ||
| 34 | } | ||
| 35 | |||
| 36 | let db_path = config.server_directory.join("mcui.db"); | ||
| 37 | let db = Database::open(&db_path).unwrap_or_else(|e| { | ||
| 38 | eprintln!("Failed to open database: {e}"); | ||
| 39 | std::process::exit(1); | ||
| 40 | }); | ||
| 41 | |||
| 42 | bootstrap_superuser(&db, &config); | ||
| 43 | |||
| 44 | let docker = DockerManager::new(config.clone()).unwrap_or_else(|e| { | ||
| 45 | eprintln!("Failed to connect to Docker: {e}"); | ||
| 46 | eprintln!("Is the Docker daemon running and accessible?"); | ||
| 47 | std::process::exit(1); | ||
| 48 | }); | ||
| 49 | |||
| 50 | let state = AppState { | ||
| 51 | config: config.clone(), | ||
| 52 | db: db.clone(), | ||
| 53 | docker, | ||
| 54 | }; | ||
| 55 | |||
| 56 | let router = routes::build_router(state); | ||
| 57 | |||
| 58 | let addr = format!("127.0.0.1:{}", config.webui_port); | ||
| 59 | tracing::info!("Starting Minecraft UI on http://{addr}"); | ||
| 60 | |||
| 61 | let listener = tokio::net::TcpListener::bind(&addr) | ||
| 62 | .await | ||
| 63 | .unwrap_or_else(|e| { | ||
| 64 | eprintln!("Failed to bind to {addr}: {e}"); | ||
| 65 | std::process::exit(1); | ||
| 66 | }); | ||
| 67 | |||
| 68 | axum::serve(listener, router).await.unwrap_or_else(|e| { | ||
| 69 | eprintln!("Server error: {e}"); | ||
| 70 | }); | ||
| 71 | } | ||
| 72 | |||
| 73 | fn bootstrap_superuser(db: &Database, config: &config::Config) { | ||
| 74 | let conn = db.conn(); | ||
| 75 | let count: i32 = conn | ||
| 76 | .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0)) | ||
| 77 | .unwrap_or(0); | ||
| 78 | drop(conn); | ||
| 79 | |||
| 80 | if count == 0 { | ||
| 81 | let hash = auth::hash_password(&config.initial_superuser_password) | ||
| 82 | .expect("Failed to hash initial password"); | ||
| 83 | |||
| 84 | let conn = db.conn(); | ||
| 85 | conn.execute( | ||
| 86 | "INSERT INTO accounts (username, password, is_superuser, password_change_required) | ||
| 87 | VALUES (?1, ?2, 1, 1)", | ||
| 88 | rusqlite::params![config.initial_superuser, hash], | ||
| 89 | ) | ||
| 90 | .expect("Failed to create initial superuser"); | ||
| 91 | drop(conn); | ||
| 92 | |||
| 93 | tracing::info!( | ||
| 94 | "Created initial superuser '{}' (password change required on first login)", | ||
| 95 | config.initial_superuser | ||
| 96 | ); | ||
| 97 | } | ||
| 3 | } | 98 | } |
diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..c847ea3 --- /dev/null +++ b/src/models/mod.rs | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | use serde::{Deserialize, Serialize}; | ||
| 2 | |||
| 3 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 4 | pub struct Account { | ||
| 5 | pub id: i64, | ||
| 6 | pub username: String, | ||
| 7 | pub is_superuser: bool, | ||
| 8 | pub password_change_required: bool, | ||
| 9 | } | ||
| 10 | |||
| 11 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 12 | pub struct Instance { | ||
| 13 | pub id: i64, | ||
| 14 | pub name: String, | ||
| 15 | pub version: String, | ||
| 16 | pub server_type: String, | ||
| 17 | pub world_source: String, | ||
| 18 | pub world_seed: Option<String>, | ||
| 19 | pub level_type: String, | ||
| 20 | pub has_modpack: bool, | ||
| 21 | pub memory_mb: i32, | ||
| 22 | pub difficulty: i32, | ||
| 23 | pub gamemode: i32, | ||
| 24 | pub max_players: i32, | ||
| 25 | pub pvp: bool, | ||
| 26 | pub motd: String, | ||
| 27 | pub rcon_password: String, | ||
| 28 | pub is_active: bool, | ||
| 29 | pub created_at: String, | ||
| 30 | pub updated_at: String, | ||
| 31 | } | ||
| 32 | |||
| 33 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 34 | pub struct Backup { | ||
| 35 | pub id: i64, | ||
| 36 | pub instance_id: i64, | ||
| 37 | pub filename: String, | ||
| 38 | pub created_at: String, | ||
| 39 | } | ||
| 40 | |||
| 41 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 42 | pub struct PlayerEntry { | ||
| 43 | pub username: String, | ||
| 44 | } | ||
diff --git a/src/rcon.rs b/src/rcon.rs new file mode 100644 index 0000000..b91fe9b --- /dev/null +++ b/src/rcon.rs | |||
| @@ -0,0 +1,133 @@ | |||
| 1 | use std::io::{Read, Write}; | ||
| 2 | use std::net::TcpStream; | ||
| 3 | |||
| 4 | #[derive(Debug, Clone)] | ||
| 5 | pub struct RconClient { | ||
| 6 | host: String, | ||
| 7 | port: u16, | ||
| 8 | password: String, | ||
| 9 | } | ||
| 10 | |||
| 11 | #[derive(Debug)] | ||
| 12 | pub enum RconError { | ||
| 13 | Connection(String), | ||
| 14 | Auth(String), | ||
| 15 | Response(String), | ||
| 16 | } | ||
| 17 | |||
| 18 | impl std::fmt::Display for RconError { | ||
| 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 20 | match self { | ||
| 21 | RconError::Connection(e) => write!(f, "RCON connection error: {e}"), | ||
| 22 | RconError::Auth(e) => write!(f, "RCON auth error: {e}"), | ||
| 23 | RconError::Response(e) => write!(f, "RCON response error: {e}"), | ||
| 24 | } | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | const SERVERDATA_AUTH: i32 = 3; | ||
| 29 | const SERVERDATA_EXECCOMMAND: i32 = 2; | ||
| 30 | |||
| 31 | impl RconClient { | ||
| 32 | pub fn new(password: String) -> Self { | ||
| 33 | Self { | ||
| 34 | host: "127.0.0.1".to_string(), | ||
| 35 | port: 25575, | ||
| 36 | password, | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | /// Non-blocking async wrapper — runs the blocking RCON call on a thread pool. | ||
| 41 | pub async fn send_command_async(&self, command: String) -> Result<String, RconError> { | ||
| 42 | let client = self.clone(); | ||
| 43 | tokio::task::spawn_blocking(move || client.send_command(&command)) | ||
| 44 | .await | ||
| 45 | .map_err(|e| RconError::Connection(e.to_string()))? | ||
| 46 | } | ||
| 47 | |||
| 48 | fn send_command(&self, command: &str) -> Result<String, RconError> { | ||
| 49 | let mut stream = TcpStream::connect(format!("{}:{}", self.host, self.port)) | ||
| 50 | .map_err(|e| RconError::Connection(e.to_string()))?; | ||
| 51 | |||
| 52 | stream | ||
| 53 | .set_read_timeout(Some(std::time::Duration::from_secs(1))) | ||
| 54 | .ok(); | ||
| 55 | |||
| 56 | let request_id = 1; | ||
| 57 | self.write_packet(&mut stream, request_id, SERVERDATA_AUTH, &self.password)?; | ||
| 58 | let response = self.read_packet(&mut stream)?; | ||
| 59 | |||
| 60 | if response.request_id == -1 { | ||
| 61 | return Err(RconError::Auth("Invalid RCON password".into())); | ||
| 62 | } | ||
| 63 | |||
| 64 | let cmd_id = 2; | ||
| 65 | self.write_packet(&mut stream, cmd_id, SERVERDATA_EXECCOMMAND, command)?; | ||
| 66 | |||
| 67 | let response = self.read_packet(&mut stream)?; | ||
| 68 | // Second packet is an end marker — some servers send it, some don't; ignore errors | ||
| 69 | let _ = self.read_packet(&mut stream); | ||
| 70 | |||
| 71 | Ok(response.body) | ||
| 72 | } | ||
| 73 | |||
| 74 | fn write_packet( | ||
| 75 | &self, | ||
| 76 | stream: &mut TcpStream, | ||
| 77 | request_id: i32, | ||
| 78 | packet_type: i32, | ||
| 79 | body: &str, | ||
| 80 | ) -> Result<(), RconError> { | ||
| 81 | let body_bytes = body.as_bytes(); | ||
| 82 | let length = 10 + body_bytes.len() as i32; | ||
| 83 | |||
| 84 | let mut packet = Vec::with_capacity(4 + length as usize); | ||
| 85 | packet.extend_from_slice(&length.to_le_bytes()); | ||
| 86 | packet.extend_from_slice(&request_id.to_le_bytes()); | ||
| 87 | packet.extend_from_slice(&packet_type.to_le_bytes()); | ||
| 88 | packet.extend_from_slice(body_bytes); | ||
| 89 | packet.extend_from_slice(&[0u8, 0u8]); | ||
| 90 | |||
| 91 | stream | ||
| 92 | .write_all(&packet) | ||
| 93 | .map_err(|e| RconError::Connection(e.to_string()))?; | ||
| 94 | |||
| 95 | Ok(()) | ||
| 96 | } | ||
| 97 | |||
| 98 | fn read_packet(&self, stream: &mut TcpStream) -> Result<RconPacket, RconError> { | ||
| 99 | let mut length_buf = [0u8; 4]; | ||
| 100 | stream | ||
| 101 | .read_exact(&mut length_buf) | ||
| 102 | .map_err(|e| RconError::Response(format!("Failed to read length: {e}")))?; | ||
| 103 | |||
| 104 | let length = i32::from_le_bytes(length_buf); | ||
| 105 | if length < 10 { | ||
| 106 | return Err(RconError::Response("Packet too short".into())); | ||
| 107 | } | ||
| 108 | |||
| 109 | let mut payload = vec![0u8; length as usize]; | ||
| 110 | stream | ||
| 111 | .read_exact(&mut payload) | ||
| 112 | .map_err(|e| RconError::Response(format!("Failed to read payload: {e}")))?; | ||
| 113 | |||
| 114 | let request_id = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); | ||
| 115 | let packet_type = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); | ||
| 116 | |||
| 117 | let body_bytes = &payload[8..payload.len() - 2]; | ||
| 118 | let body = String::from_utf8_lossy(body_bytes).to_string(); | ||
| 119 | |||
| 120 | Ok(RconPacket { | ||
| 121 | request_id, | ||
| 122 | packet_type, | ||
| 123 | body, | ||
| 124 | }) | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | #[derive(Debug)] | ||
| 129 | struct RconPacket { | ||
| 130 | request_id: i32, | ||
| 131 | packet_type: i32, | ||
| 132 | body: String, | ||
| 133 | } | ||
diff --git a/src/routes/accounts.rs b/src/routes/accounts.rs new file mode 100644 index 0000000..89774f8 --- /dev/null +++ b/src/routes/accounts.rs | |||
| @@ -0,0 +1,236 @@ | |||
| 1 | use askama::Template; | ||
| 2 | use axum::{ | ||
| 3 | Form, Router, | ||
| 4 | extract::State, | ||
| 5 | response::{Html, Redirect}, | ||
| 6 | routing::{get, post}, | ||
| 7 | }; | ||
| 8 | use rusqlite::params; | ||
| 9 | use serde::Deserialize; | ||
| 10 | |||
| 11 | use crate::auth::{CurrentUser, hash_password}; | ||
| 12 | use crate::routes::AppState; | ||
| 13 | |||
| 14 | #[derive(Template)] | ||
| 15 | #[template(path = "accounts/manage.html")] | ||
| 16 | struct AccountManagementTemplate { | ||
| 17 | username: String, | ||
| 18 | is_superuser: bool, | ||
| 19 | password_change_required: bool, | ||
| 20 | accounts: Vec<AccountRow>, | ||
| 21 | message: Option<String>, | ||
| 22 | } | ||
| 23 | |||
| 24 | struct AccountRow { | ||
| 25 | id: i64, | ||
| 26 | username: String, | ||
| 27 | is_superuser: bool, | ||
| 28 | } | ||
| 29 | |||
| 30 | #[derive(Debug, Deserialize)] | ||
| 31 | pub struct CreateAccountForm { | ||
| 32 | pub username: String, | ||
| 33 | pub password: String, | ||
| 34 | pub is_superuser: Option<String>, | ||
| 35 | } | ||
| 36 | |||
| 37 | #[derive(Debug, Deserialize)] | ||
| 38 | pub struct DeleteAccountForm { | ||
| 39 | pub id: i64, | ||
| 40 | } | ||
| 41 | |||
| 42 | #[derive(Debug, Deserialize)] | ||
| 43 | pub struct ChangePasswordForm { | ||
| 44 | pub current_password: String, | ||
| 45 | pub new_password: String, | ||
| 46 | } | ||
| 47 | |||
| 48 | #[derive(Debug, Deserialize)] | ||
| 49 | pub struct SetPasswordForm { | ||
| 50 | pub id: i64, | ||
| 51 | pub new_password: String, | ||
| 52 | } | ||
| 53 | |||
| 54 | pub fn router() -> Router<AppState> { | ||
| 55 | Router::new() | ||
| 56 | .route("/accounts", get(manage_accounts)) | ||
| 57 | .route("/accounts/create", post(create_account)) | ||
| 58 | .route("/accounts/delete", post(delete_account)) | ||
| 59 | .route("/accounts/set-password", post(set_password)) | ||
| 60 | .route( | ||
| 61 | "/accounts/change-own-password", | ||
| 62 | post(change_own_password_from_accounts), | ||
| 63 | ) | ||
| 64 | } | ||
| 65 | |||
| 66 | pub async fn change_password( | ||
| 67 | State(state): State<AppState>, | ||
| 68 | CurrentUser(account): CurrentUser, | ||
| 69 | Form(form): Form<ChangePasswordForm>, | ||
| 70 | ) -> Result<Redirect, Redirect> { | ||
| 71 | let conn = state.db.conn(); | ||
| 72 | |||
| 73 | let hash: String = conn | ||
| 74 | .query_row( | ||
| 75 | "SELECT password FROM accounts WHERE id = ?1", | ||
| 76 | params![account.id], | ||
| 77 | |row| row.get(0), | ||
| 78 | ) | ||
| 79 | .map_err(|_| Redirect::to("/login"))?; | ||
| 80 | |||
| 81 | if !crate::auth::verify_password(&form.current_password, &hash).unwrap_or(false) { | ||
| 82 | drop(conn); | ||
| 83 | return Ok(Redirect::to("/change-password")); | ||
| 84 | } | ||
| 85 | |||
| 86 | let new_hash = hash_password(&form.new_password).map_err(|_| Redirect::to("/login"))?; | ||
| 87 | |||
| 88 | conn.execute( | ||
| 89 | "UPDATE accounts SET password = ?1, password_change_required = 0 WHERE id = ?2", | ||
| 90 | params![new_hash, account.id], | ||
| 91 | ) | ||
| 92 | .map_err(|_| Redirect::to("/login"))?; | ||
| 93 | |||
| 94 | conn.execute( | ||
| 95 | "DELETE FROM sessions WHERE account_id = ?1", | ||
| 96 | params![account.id], | ||
| 97 | ) | ||
| 98 | .ok(); | ||
| 99 | drop(conn); | ||
| 100 | |||
| 101 | Ok(Redirect::to("/login")) | ||
| 102 | } | ||
| 103 | |||
| 104 | async fn manage_accounts( | ||
| 105 | State(state): State<AppState>, | ||
| 106 | CurrentUser(account): CurrentUser, | ||
| 107 | ) -> Result<Html<String>, Redirect> { | ||
| 108 | if !account.is_superuser { | ||
| 109 | return Err(Redirect::to("/")); | ||
| 110 | } | ||
| 111 | |||
| 112 | let conn = state.db.conn(); | ||
| 113 | let mut stmt = conn | ||
| 114 | .prepare("SELECT id, username, is_superuser FROM accounts ORDER BY username") | ||
| 115 | .map_err(|_| Redirect::to("/login"))?; | ||
| 116 | |||
| 117 | let accounts: Vec<AccountRow> = stmt | ||
| 118 | .query_map([], |row| { | ||
| 119 | Ok(AccountRow { | ||
| 120 | id: row.get(0)?, | ||
| 121 | username: row.get(1)?, | ||
| 122 | is_superuser: row.get::<_, i32>(2)? != 0, | ||
| 123 | }) | ||
| 124 | }) | ||
| 125 | .map_err(|_| Redirect::to("/login"))? | ||
| 126 | .filter_map(|r| r.ok()) | ||
| 127 | .collect(); | ||
| 128 | drop(stmt); | ||
| 129 | drop(conn); | ||
| 130 | |||
| 131 | let template = AccountManagementTemplate { | ||
| 132 | username: account.username, | ||
| 133 | is_superuser: account.is_superuser, | ||
| 134 | password_change_required: account.password_change_required, | ||
| 135 | accounts, | ||
| 136 | message: None, | ||
| 137 | }; | ||
| 138 | |||
| 139 | Ok(Html(template.render().unwrap_or_default())) | ||
| 140 | } | ||
| 141 | |||
| 142 | async fn create_account( | ||
| 143 | State(state): State<AppState>, | ||
| 144 | CurrentUser(account): CurrentUser, | ||
| 145 | Form(form): Form<CreateAccountForm>, | ||
| 146 | ) -> Result<Redirect, Redirect> { | ||
| 147 | if !account.is_superuser { | ||
| 148 | return Ok(Redirect::to("/")); | ||
| 149 | } | ||
| 150 | |||
| 151 | let hash = hash_password(&form.password).map_err(|_| Redirect::to("/accounts"))?; | ||
| 152 | let is_superuser = form.is_superuser.as_deref() == Some("on"); | ||
| 153 | |||
| 154 | let conn = state.db.conn(); | ||
| 155 | let _ = conn.execute( | ||
| 156 | "INSERT INTO accounts (username, password, is_superuser) VALUES (?1, ?2, ?3)", | ||
| 157 | params![form.username, hash, is_superuser], | ||
| 158 | ); | ||
| 159 | drop(conn); | ||
| 160 | |||
| 161 | Ok(Redirect::to("/accounts")) | ||
| 162 | } | ||
| 163 | |||
| 164 | async fn delete_account( | ||
| 165 | State(state): State<AppState>, | ||
| 166 | CurrentUser(account): CurrentUser, | ||
| 167 | Form(form): Form<DeleteAccountForm>, | ||
| 168 | ) -> Result<Redirect, Redirect> { | ||
| 169 | if !account.is_superuser || account.id == form.id { | ||
| 170 | return Ok(Redirect::to("/accounts")); | ||
| 171 | } | ||
| 172 | |||
| 173 | let conn = state.db.conn(); | ||
| 174 | let superuser_count: i32 = conn | ||
| 175 | .query_row( | ||
| 176 | "SELECT COUNT(*) FROM accounts WHERE is_superuser = 1 AND id != ?1", | ||
| 177 | params![form.id], | ||
| 178 | |row| row.get(0), | ||
| 179 | ) | ||
| 180 | .unwrap_or(0); | ||
| 181 | |||
| 182 | let is_superuser: bool = conn | ||
| 183 | .query_row( | ||
| 184 | "SELECT is_superuser FROM accounts WHERE id = ?1", | ||
| 185 | params![form.id], | ||
| 186 | |row| row.get::<_, i32>(0).map(|v| v != 0), | ||
| 187 | ) | ||
| 188 | .unwrap_or(false); | ||
| 189 | |||
| 190 | if is_superuser && superuser_count == 0 { | ||
| 191 | drop(conn); | ||
| 192 | return Ok(Redirect::to("/accounts")); | ||
| 193 | } | ||
| 194 | |||
| 195 | let _ = conn.execute("DELETE FROM accounts WHERE id = ?1", params![form.id]); | ||
| 196 | drop(conn); | ||
| 197 | |||
| 198 | Ok(Redirect::to("/accounts")) | ||
| 199 | } | ||
| 200 | |||
| 201 | async fn set_password( | ||
| 202 | State(state): State<AppState>, | ||
| 203 | CurrentUser(account): CurrentUser, | ||
| 204 | Form(form): Form<SetPasswordForm>, | ||
| 205 | ) -> Result<Redirect, Redirect> { | ||
| 206 | if !account.is_superuser { | ||
| 207 | return Ok(Redirect::to("/")); | ||
| 208 | } | ||
| 209 | |||
| 210 | let new_hash = hash_password(&form.new_password).map_err(|_| Redirect::to("/accounts"))?; | ||
| 211 | |||
| 212 | let conn = state.db.conn(); | ||
| 213 | conn.execute( | ||
| 214 | "UPDATE accounts SET password = ?1, password_change_required = 0 WHERE id = ?2", | ||
| 215 | params![new_hash, form.id], | ||
| 216 | ) | ||
| 217 | .ok(); | ||
| 218 | |||
| 219 | // Invalidate sessions for the target account | ||
| 220 | conn.execute( | ||
| 221 | "DELETE FROM sessions WHERE account_id = ?1", | ||
| 222 | params![form.id], | ||
| 223 | ) | ||
| 224 | .ok(); | ||
| 225 | drop(conn); | ||
| 226 | |||
| 227 | Ok(Redirect::to("/accounts")) | ||
| 228 | } | ||
| 229 | |||
| 230 | async fn change_own_password_from_accounts( | ||
| 231 | State(state): State<AppState>, | ||
| 232 | CurrentUser(account): CurrentUser, | ||
| 233 | Form(form): Form<ChangePasswordForm>, | ||
| 234 | ) -> Result<Redirect, Redirect> { | ||
| 235 | change_password(State(state), CurrentUser(account), Form(form)).await | ||
| 236 | } | ||
diff --git a/src/routes/backups.rs b/src/routes/backups.rs new file mode 100644 index 0000000..09298d2 --- /dev/null +++ b/src/routes/backups.rs | |||
| @@ -0,0 +1,256 @@ | |||
| 1 | use askama::Template; | ||
| 2 | use axum::{ | ||
| 3 | Router, | ||
| 4 | body::Body, | ||
| 5 | extract::{Path, State}, | ||
| 6 | http::{StatusCode, header}, | ||
| 7 | response::{Html, Redirect, Response}, | ||
| 8 | routing::{get, post}, | ||
| 9 | }; | ||
| 10 | use rusqlite::params; | ||
| 11 | use zip::write::FileOptions; | ||
| 12 | |||
| 13 | use crate::auth::CurrentUser; | ||
| 14 | use crate::routes::AppState; | ||
| 15 | |||
| 16 | #[derive(Template)] | ||
| 17 | #[template(path = "backups/list.html")] | ||
| 18 | struct BackupListTemplate { | ||
| 19 | username: String, | ||
| 20 | is_superuser: bool, | ||
| 21 | password_change_required: bool, | ||
| 22 | instance_name: String, | ||
| 23 | instance_id: i64, | ||
| 24 | backups: Vec<BackupRow>, | ||
| 25 | message: Option<String>, | ||
| 26 | } | ||
| 27 | |||
| 28 | struct BackupRow { | ||
| 29 | id: i64, | ||
| 30 | filename: String, | ||
| 31 | created_at: String, | ||
| 32 | } | ||
| 33 | |||
| 34 | pub fn router() -> Router<AppState> { | ||
| 35 | Router::new() | ||
| 36 | .route("/instances/{id}/backups", get(list_backups)) | ||
| 37 | .route("/instances/{id}/backups/create", post(create_backup)) | ||
| 38 | .route("/backups/{id}/restore", post(restore_backup)) | ||
| 39 | .route("/backups/{id}/download", get(download_backup)) | ||
| 40 | .route("/instances/{id}/world/export", get(export_world)) | ||
| 41 | } | ||
| 42 | |||
| 43 | async fn list_backups( | ||
| 44 | State(state): State<AppState>, | ||
| 45 | Path(instance_id): Path<i64>, | ||
| 46 | CurrentUser(account): CurrentUser, | ||
| 47 | ) -> Result<Html<String>, axum::response::Redirect> { | ||
| 48 | let conn = state.db.conn(); | ||
| 49 | let instance_name: String = conn | ||
| 50 | .query_row( | ||
| 51 | "SELECT name FROM instances WHERE id = ?1", | ||
| 52 | params![instance_id], | ||
| 53 | |row| row.get(0), | ||
| 54 | ) | ||
| 55 | .map_err(|_| Redirect::to("/instances"))?; | ||
| 56 | |||
| 57 | let mut stmt = conn | ||
| 58 | .prepare("SELECT id, filename, created_at FROM backups WHERE instance_id = ?1 ORDER BY created_at DESC") | ||
| 59 | .map_err(|_| Redirect::to("/instances"))?; | ||
| 60 | |||
| 61 | let backups: Vec<BackupRow> = stmt | ||
| 62 | .query_map(params![instance_id], |row| { | ||
| 63 | Ok(BackupRow { | ||
| 64 | id: row.get(0)?, | ||
| 65 | filename: row.get(1)?, | ||
| 66 | created_at: row.get(2)?, | ||
| 67 | }) | ||
| 68 | }) | ||
| 69 | .map_err(|_| Redirect::to("/instances"))? | ||
| 70 | .filter_map(|r| r.ok()) | ||
| 71 | .collect(); | ||
| 72 | drop(stmt); | ||
| 73 | drop(conn); | ||
| 74 | |||
| 75 | let template = BackupListTemplate { | ||
| 76 | username: account.username.clone(), | ||
| 77 | is_superuser: account.is_superuser, | ||
| 78 | password_change_required: account.password_change_required, | ||
| 79 | instance_name, | ||
| 80 | instance_id, | ||
| 81 | backups, | ||
| 82 | message: None, | ||
| 83 | }; | ||
| 84 | |||
| 85 | Ok(Html(template.render().unwrap_or_default())) | ||
| 86 | } | ||
| 87 | |||
| 88 | async fn create_backup( | ||
| 89 | State(state): State<AppState>, | ||
| 90 | Path(instance_id): Path<i64>, | ||
| 91 | CurrentUser(_user): CurrentUser, | ||
| 92 | ) -> Result<Redirect, axum::response::Redirect> { | ||
| 93 | let conn = state.db.conn(); | ||
| 94 | let instance_name: String = conn | ||
| 95 | .query_row( | ||
| 96 | "SELECT name FROM instances WHERE id = ?1", | ||
| 97 | params![instance_id], | ||
| 98 | |row| row.get(0), | ||
| 99 | ) | ||
| 100 | .map_err(|_| Redirect::to("/instances"))?; | ||
| 101 | drop(conn); | ||
| 102 | |||
| 103 | let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); | ||
| 104 | let filename = format!("{instance_name}_{timestamp}.zip"); | ||
| 105 | |||
| 106 | let data_dir = state | ||
| 107 | .config | ||
| 108 | .server_directory | ||
| 109 | .join("instances") | ||
| 110 | .join(&instance_name) | ||
| 111 | .join("data"); | ||
| 112 | |||
| 113 | let backup_dir = state.config.server_directory.join("backups"); | ||
| 114 | let _ = std::fs::create_dir_all(&backup_dir); | ||
| 115 | let backup_path = backup_dir.join(&filename); | ||
| 116 | |||
| 117 | if let Ok(file) = std::fs::File::create(&backup_path) { | ||
| 118 | let mut zip = zip::ZipWriter::new(file); | ||
| 119 | let options: FileOptions<'_, ()> = | ||
| 120 | FileOptions::default().compression_method(zip::CompressionMethod::Deflated); | ||
| 121 | |||
| 122 | if let Ok(entries) = std::fs::read_dir(&data_dir) { | ||
| 123 | for entry in entries.flatten() { | ||
| 124 | let path = entry.path(); | ||
| 125 | if path.is_file() { | ||
| 126 | if let Some(name) = path.file_name() { | ||
| 127 | if let Ok(contents) = std::fs::read(&path) { | ||
| 128 | let _ = zip.start_file(&*name.to_string_lossy(), options); | ||
| 129 | let _ = std::io::Write::write_all(&mut zip, &contents); | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | let _ = zip.finish(); | ||
| 136 | } | ||
| 137 | |||
| 138 | let conn = state.db.conn(); | ||
| 139 | conn.execute( | ||
| 140 | "INSERT INTO backups (instance_id, filename) VALUES (?1, ?2)", | ||
| 141 | params![instance_id, filename], | ||
| 142 | ) | ||
| 143 | .ok(); | ||
| 144 | drop(conn); | ||
| 145 | |||
| 146 | Ok(Redirect::to(&format!("/instances/{instance_id}/backups"))) | ||
| 147 | } | ||
| 148 | |||
| 149 | async fn restore_backup( | ||
| 150 | State(state): State<AppState>, | ||
| 151 | Path(backup_id): Path<i64>, | ||
| 152 | CurrentUser(_user): CurrentUser, | ||
| 153 | ) -> Result<Redirect, axum::response::Redirect> { | ||
| 154 | let conn = state.db.conn(); | ||
| 155 | let (instance_id, _filename): (i64, String) = conn | ||
| 156 | .query_row( | ||
| 157 | "SELECT instance_id, filename FROM backups WHERE id = ?1", | ||
| 158 | params![backup_id], | ||
| 159 | |row| Ok((row.get(0)?, row.get(1)?)), | ||
| 160 | ) | ||
| 161 | .map_err(|_| Redirect::to("/instances"))?; | ||
| 162 | drop(conn); | ||
| 163 | |||
| 164 | Ok(Redirect::to(&format!("/instances/{instance_id}/backups"))) | ||
| 165 | } | ||
| 166 | |||
| 167 | async fn download_backup( | ||
| 168 | State(state): State<AppState>, | ||
| 169 | Path(backup_id): Path<i64>, | ||
| 170 | CurrentUser(_user): CurrentUser, | ||
| 171 | ) -> Result<Response, (StatusCode, &'static str)> { | ||
| 172 | let conn = state.db.conn(); | ||
| 173 | let (_, filename): (i64, String) = conn | ||
| 174 | .query_row( | ||
| 175 | "SELECT instance_id, filename FROM backups WHERE id = ?1", | ||
| 176 | params![backup_id], | ||
| 177 | |row| Ok((row.get(0)?, row.get(1)?)), | ||
| 178 | ) | ||
| 179 | .map_err(|_| (StatusCode::NOT_FOUND, "Backup not found"))?; | ||
| 180 | drop(conn); | ||
| 181 | |||
| 182 | let backup_path = state | ||
| 183 | .config | ||
| 184 | .server_directory | ||
| 185 | .join("backups") | ||
| 186 | .join(&filename); | ||
| 187 | |||
| 188 | let content = | ||
| 189 | std::fs::read(&backup_path).map_err(|_| (StatusCode::NOT_FOUND, "File not found"))?; | ||
| 190 | |||
| 191 | Ok(Response::builder() | ||
| 192 | .header(header::CONTENT_TYPE, "application/zip") | ||
| 193 | .header( | ||
| 194 | header::CONTENT_DISPOSITION, | ||
| 195 | format!("attachment; filename=\"{filename}\""), | ||
| 196 | ) | ||
| 197 | .body(Body::from(content)) | ||
| 198 | .unwrap()) | ||
| 199 | } | ||
| 200 | |||
| 201 | async fn export_world( | ||
| 202 | State(state): State<AppState>, | ||
| 203 | Path(instance_id): Path<i64>, | ||
| 204 | CurrentUser(_user): CurrentUser, | ||
| 205 | ) -> Result<Response, (StatusCode, &'static str)> { | ||
| 206 | let conn = state.db.conn(); | ||
| 207 | let instance_name: String = conn | ||
| 208 | .query_row( | ||
| 209 | "SELECT name FROM instances WHERE id = ?1", | ||
| 210 | params![instance_id], | ||
| 211 | |row| row.get(0), | ||
| 212 | ) | ||
| 213 | .map_err(|_| (StatusCode::NOT_FOUND, "Instance not found"))?; | ||
| 214 | drop(conn); | ||
| 215 | |||
| 216 | let world_dir = state | ||
| 217 | .config | ||
| 218 | .server_directory | ||
| 219 | .join("instances") | ||
| 220 | .join(&instance_name) | ||
| 221 | .join("data") | ||
| 222 | .join("world"); | ||
| 223 | |||
| 224 | let mut zip_buf = Vec::new(); | ||
| 225 | { | ||
| 226 | let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut zip_buf)); | ||
| 227 | let options: FileOptions<'_, ()> = | ||
| 228 | FileOptions::default().compression_method(zip::CompressionMethod::Deflated); | ||
| 229 | |||
| 230 | if let Ok(entries) = std::fs::read_dir(&world_dir) { | ||
| 231 | for entry in entries.flatten() { | ||
| 232 | let path = entry.path(); | ||
| 233 | if path.is_file() { | ||
| 234 | if let Some(name) = path.file_name() { | ||
| 235 | if let Ok(contents) = std::fs::read(&path) { | ||
| 236 | let _ = zip.start_file(&*name.to_string_lossy(), options); | ||
| 237 | let _ = std::io::Write::write_all(&mut zip, &contents); | ||
| 238 | } | ||
| 239 | } | ||
| 240 | } | ||
| 241 | } | ||
| 242 | } | ||
| 243 | let _ = zip.finish(); | ||
| 244 | } | ||
| 245 | |||
| 246 | let filename = format!("{instance_name}_world.zip"); | ||
| 247 | |||
| 248 | Ok(Response::builder() | ||
| 249 | .header(header::CONTENT_TYPE, "application/zip") | ||
| 250 | .header( | ||
| 251 | header::CONTENT_DISPOSITION, | ||
| 252 | format!("attachment; filename=\"{filename}\""), | ||
| 253 | ) | ||
| 254 | .body(Body::from(zip_buf)) | ||
| 255 | .unwrap()) | ||
| 256 | } | ||
diff --git a/src/routes/dashboard.rs b/src/routes/dashboard.rs new file mode 100644 index 0000000..7d5d3e4 --- /dev/null +++ b/src/routes/dashboard.rs | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | use askama::Template; | ||
| 2 | use axum::{ | ||
| 3 | Router, | ||
| 4 | extract::State, | ||
| 5 | response::{Html, IntoResponse}, | ||
| 6 | routing::get, | ||
| 7 | }; | ||
| 8 | |||
| 9 | use crate::auth::CurrentUser; | ||
| 10 | use crate::routes::AppState; | ||
| 11 | |||
| 12 | #[derive(Template)] | ||
| 13 | #[template(path = "dashboard.html")] | ||
| 14 | struct DashboardTemplate { | ||
| 15 | username: String, | ||
| 16 | is_superuser: bool, | ||
| 17 | password_change_required: bool, | ||
| 18 | active_instance: Option<ActiveInstanceInfo>, | ||
| 19 | container_state: String, | ||
| 20 | } | ||
| 21 | |||
| 22 | #[derive(Clone)] | ||
| 23 | struct ActiveInstanceInfo { | ||
| 24 | name: String, | ||
| 25 | version: String, | ||
| 26 | server_type: String, | ||
| 27 | max_players: i32, | ||
| 28 | } | ||
| 29 | |||
| 30 | pub fn router() -> Router<AppState> { | ||
| 31 | Router::new().route("/", get(dashboard)) | ||
| 32 | } | ||
| 33 | |||
| 34 | async fn dashboard( | ||
| 35 | State(state): State<AppState>, | ||
| 36 | CurrentUser(account): CurrentUser, | ||
| 37 | ) -> Result<impl IntoResponse, axum::response::Redirect> { | ||
| 38 | let (active_info, container_state) = { | ||
| 39 | let conn = state.db.conn(); | ||
| 40 | let result = conn.query_row( | ||
| 41 | "SELECT name, version, server_type, max_players, container_state | ||
| 42 | FROM instances WHERE is_active = 1", | ||
| 43 | [], | ||
| 44 | |row| { | ||
| 45 | Ok(( | ||
| 46 | Some(ActiveInstanceInfo { | ||
| 47 | name: row.get(0)?, | ||
| 48 | version: row.get(1)?, | ||
| 49 | server_type: row.get(2)?, | ||
| 50 | max_players: row.get(3)?, | ||
| 51 | }), | ||
| 52 | row.get::<_, String>(4)?, | ||
| 53 | )) | ||
| 54 | }, | ||
| 55 | ); | ||
| 56 | result.unwrap_or((None, "stopped".into())) | ||
| 57 | }; | ||
| 58 | |||
| 59 | let template = DashboardTemplate { | ||
| 60 | username: account.username, | ||
| 61 | is_superuser: account.is_superuser, | ||
| 62 | password_change_required: account.password_change_required, | ||
| 63 | active_instance: active_info, | ||
| 64 | container_state, | ||
| 65 | }; | ||
| 66 | |||
| 67 | Ok(Html(template.render().unwrap_or_default())) | ||
| 68 | } | ||
diff --git a/src/routes/instances.rs b/src/routes/instances.rs new file mode 100644 index 0000000..2da1319 --- /dev/null +++ b/src/routes/instances.rs | |||
| @@ -0,0 +1,484 @@ | |||
| 1 | use askama::Template; | ||
| 2 | use axum::{ | ||
| 3 | Form, Router, | ||
| 4 | extract::{Path, State}, | ||
| 5 | response::{Html, Redirect}, | ||
| 6 | routing::{get, post}, | ||
| 7 | }; | ||
| 8 | use rusqlite::params; | ||
| 9 | use serde::Deserialize; | ||
| 10 | |||
| 11 | use crate::auth::CurrentUser; | ||
| 12 | use crate::models::Instance; | ||
| 13 | use crate::routes::AppState; | ||
| 14 | |||
| 15 | #[derive(Template)] | ||
| 16 | #[template(path = "instances/list.html")] | ||
| 17 | struct InstanceListTemplate { | ||
| 18 | username: String, | ||
| 19 | is_superuser: bool, | ||
| 20 | password_change_required: bool, | ||
| 21 | instances: Vec<InstanceInfo>, | ||
| 22 | has_active: bool, | ||
| 23 | } | ||
| 24 | |||
| 25 | #[derive(Template)] | ||
| 26 | #[template(path = "instances/create.html")] | ||
| 27 | struct CreateInstanceTemplate { | ||
| 28 | username: String, | ||
| 29 | is_superuser: bool, | ||
| 30 | password_change_required: bool, | ||
| 31 | versions: Vec<String>, | ||
| 32 | max_memory_mb: u32, | ||
| 33 | error: Option<String>, | ||
| 34 | } | ||
| 35 | |||
| 36 | #[derive(Template)] | ||
| 37 | #[template(path = "instances/edit.html")] | ||
| 38 | struct EditInstanceTemplate { | ||
| 39 | username: String, | ||
| 40 | is_superuser: bool, | ||
| 41 | password_change_required: bool, | ||
| 42 | instance: Instance, | ||
| 43 | max_memory_mb: u32, | ||
| 44 | error: Option<String>, | ||
| 45 | } | ||
| 46 | |||
| 47 | struct InstanceInfo { | ||
| 48 | id: i64, | ||
| 49 | name: String, | ||
| 50 | version: String, | ||
| 51 | server_type: String, | ||
| 52 | is_active: bool, | ||
| 53 | has_modpack: bool, | ||
| 54 | } | ||
| 55 | |||
| 56 | #[derive(Debug, Deserialize)] | ||
| 57 | pub struct CreateInstanceForm { | ||
| 58 | pub name: String, | ||
| 59 | pub version: String, | ||
| 60 | pub server_type: String, | ||
| 61 | pub world_source: String, | ||
| 62 | pub seed: Option<String>, | ||
| 63 | pub level_type: Option<String>, | ||
| 64 | pub memory_mb: i32, | ||
| 65 | pub difficulty: i32, | ||
| 66 | pub gamemode: i32, | ||
| 67 | pub max_players: i32, | ||
| 68 | pub pvp: Option<String>, | ||
| 69 | pub motd: Option<String>, | ||
| 70 | } | ||
| 71 | |||
| 72 | #[derive(Debug, Deserialize)] | ||
| 73 | pub struct EditInstanceForm { | ||
| 74 | pub difficulty: i32, | ||
| 75 | pub gamemode: i32, | ||
| 76 | pub max_players: i32, | ||
| 77 | pub pvp: Option<String>, | ||
| 78 | pub motd: Option<String>, | ||
| 79 | pub memory_mb: i32, | ||
| 80 | } | ||
| 81 | |||
| 82 | pub fn router() -> Router<AppState> { | ||
| 83 | Router::new() | ||
| 84 | .route("/instances", get(list_instances)) | ||
| 85 | .route("/instances/new", get(create_form)) | ||
| 86 | .route("/instances/new", post(create_instance)) | ||
| 87 | .route("/instances/{id}/edit", get(edit_form)) | ||
| 88 | .route("/instances/{id}/edit", post(edit_instance)) | ||
| 89 | .route("/instances/{id}/delete", post(delete_instance)) | ||
| 90 | .route("/instances/{id}/activate", post(activate_instance)) | ||
| 91 | .route("/instances/deactivate", post(deactivate_instance)) | ||
| 92 | } | ||
| 93 | |||
| 94 | async fn list_instances( | ||
| 95 | State(state): State<AppState>, | ||
| 96 | CurrentUser(account): CurrentUser, | ||
| 97 | ) -> Result<Html<String>, axum::response::Redirect> { | ||
| 98 | let conn = state.db.conn(); | ||
| 99 | let mut stmt = conn | ||
| 100 | .prepare("SELECT id, name, version, server_type, is_active, has_modpack FROM instances ORDER BY name") | ||
| 101 | .map_err(|_| Redirect::to("/login"))?; | ||
| 102 | |||
| 103 | let instances: Vec<InstanceInfo> = stmt | ||
| 104 | .query_map([], |row| { | ||
| 105 | Ok(InstanceInfo { | ||
| 106 | id: row.get(0)?, | ||
| 107 | name: row.get(1)?, | ||
| 108 | version: row.get(2)?, | ||
| 109 | server_type: row.get(3)?, | ||
| 110 | is_active: row.get::<_, i32>(4)? != 0, | ||
| 111 | has_modpack: row.get::<_, i32>(5)? != 0, | ||
| 112 | }) | ||
| 113 | }) | ||
| 114 | .map_err(|_| Redirect::to("/login"))? | ||
| 115 | .filter_map(|r| r.ok()) | ||
| 116 | .collect(); | ||
| 117 | |||
| 118 | let has_active = instances.iter().any(|i| i.is_active); | ||
| 119 | drop(stmt); | ||
| 120 | drop(conn); | ||
| 121 | |||
| 122 | let template = InstanceListTemplate { | ||
| 123 | username: account.username.clone(), | ||
| 124 | is_superuser: account.is_superuser, | ||
| 125 | password_change_required: account.password_change_required, | ||
| 126 | instances, | ||
| 127 | has_active, | ||
| 128 | }; | ||
| 129 | |||
| 130 | Ok(Html(template.render().unwrap_or_default())) | ||
| 131 | } | ||
| 132 | |||
| 133 | async fn create_form( | ||
| 134 | State(state): State<AppState>, | ||
| 135 | CurrentUser(account): CurrentUser, | ||
| 136 | ) -> Result<Html<String>, axum::response::Redirect> { | ||
| 137 | let versions = fetch_minecraft_versions().await.unwrap_or_else(|_| { | ||
| 138 | vec![ | ||
| 139 | "1.21".into(), | ||
| 140 | "1.20.6".into(), | ||
| 141 | "1.20.1".into(), | ||
| 142 | "1.19.4".into(), | ||
| 143 | "1.12.2".into(), | ||
| 144 | ] | ||
| 145 | }); | ||
| 146 | |||
| 147 | let template = CreateInstanceTemplate { | ||
| 148 | username: account.username.clone(), | ||
| 149 | is_superuser: account.is_superuser, | ||
| 150 | password_change_required: account.password_change_required, | ||
| 151 | versions, | ||
| 152 | max_memory_mb: state.config.max_memory_mb, | ||
| 153 | error: None, | ||
| 154 | }; | ||
| 155 | |||
| 156 | Ok(Html(template.render().unwrap_or_default())) | ||
| 157 | } | ||
| 158 | |||
| 159 | async fn create_instance( | ||
| 160 | State(state): State<AppState>, | ||
| 161 | CurrentUser(_user): CurrentUser, | ||
| 162 | Form(form): Form<CreateInstanceForm>, | ||
| 163 | ) -> Result<Redirect, axum::response::Redirect> { | ||
| 164 | if form.memory_mb > state.config.max_memory_mb as i32 || form.memory_mb < 512 { | ||
| 165 | return Ok(Redirect::to("/instances/new")); | ||
| 166 | } | ||
| 167 | |||
| 168 | let rcon_password = generate_rcon_password(); | ||
| 169 | let pvp = form.pvp.as_deref() == Some("on"); | ||
| 170 | let motd = form.motd.unwrap_or_else(|| "A Minecraft Server".into()); | ||
| 171 | let level_type = form.level_type.unwrap_or_else(|| "DEFAULT".into()); | ||
| 172 | |||
| 173 | let conn = state.db.conn(); | ||
| 174 | conn.execute( | ||
| 175 | "INSERT INTO instances (name, version, server_type, world_source, world_seed, level_type, | ||
| 176 | memory_mb, difficulty, gamemode, max_players, pvp, motd, rcon_password, has_modpack) | ||
| 177 | VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, 0)", | ||
| 178 | params![ | ||
| 179 | form.name, | ||
| 180 | form.version, | ||
| 181 | form.server_type, | ||
| 182 | form.world_source, | ||
| 183 | form.seed, | ||
| 184 | level_type, | ||
| 185 | form.memory_mb, | ||
| 186 | form.difficulty, | ||
| 187 | form.gamemode, | ||
| 188 | form.max_players, | ||
| 189 | pvp, | ||
| 190 | motd, | ||
| 191 | rcon_password, | ||
| 192 | ], | ||
| 193 | ) | ||
| 194 | .ok(); | ||
| 195 | drop(conn); | ||
| 196 | |||
| 197 | let instance_dir = state | ||
| 198 | .config | ||
| 199 | .server_directory | ||
| 200 | .join("instances") | ||
| 201 | .join(&form.name) | ||
| 202 | .join("data"); | ||
| 203 | let _ = std::fs::create_dir_all(&instance_dir); | ||
| 204 | |||
| 205 | Ok(Redirect::to("/instances")) | ||
| 206 | } | ||
| 207 | |||
| 208 | async fn edit_form( | ||
| 209 | State(state): State<AppState>, | ||
| 210 | Path(id): Path<i64>, | ||
| 211 | CurrentUser(account): CurrentUser, | ||
| 212 | ) -> Result<Html<String>, axum::response::Redirect> { | ||
| 213 | let conn = state.db.conn(); | ||
| 214 | let instance = conn | ||
| 215 | .query_row( | ||
| 216 | "SELECT id, name, version, server_type, world_source, world_seed, level_type, | ||
| 217 | has_modpack, memory_mb, difficulty, gamemode, max_players, pvp, motd, | ||
| 218 | rcon_password, is_active, created_at, updated_at | ||
| 219 | FROM instances WHERE id = ?1", | ||
| 220 | params![id], | ||
| 221 | |row| { | ||
| 222 | Ok(Instance { | ||
| 223 | id: row.get(0)?, | ||
| 224 | name: row.get(1)?, | ||
| 225 | version: row.get(2)?, | ||
| 226 | server_type: row.get(3)?, | ||
| 227 | world_source: row.get(4)?, | ||
| 228 | world_seed: row.get(5)?, | ||
| 229 | level_type: row.get(6)?, | ||
| 230 | has_modpack: row.get::<_, i32>(7)? != 0, | ||
| 231 | memory_mb: row.get(8)?, | ||
| 232 | difficulty: row.get(9)?, | ||
| 233 | gamemode: row.get(10)?, | ||
| 234 | max_players: row.get(11)?, | ||
| 235 | pvp: row.get::<_, i32>(12)? != 0, | ||
| 236 | motd: row.get(13)?, | ||
| 237 | rcon_password: row.get(14)?, | ||
| 238 | is_active: row.get::<_, i32>(15)? != 0, | ||
| 239 | created_at: row.get(16)?, | ||
| 240 | updated_at: row.get(17)?, | ||
| 241 | }) | ||
| 242 | }, | ||
| 243 | ) | ||
| 244 | .map_err(|_| Redirect::to("/instances"))?; | ||
| 245 | drop(conn); | ||
| 246 | |||
| 247 | let template = EditInstanceTemplate { | ||
| 248 | username: account.username.clone(), | ||
| 249 | is_superuser: account.is_superuser, | ||
| 250 | password_change_required: account.password_change_required, | ||
| 251 | instance, | ||
| 252 | max_memory_mb: state.config.max_memory_mb, | ||
| 253 | error: None, | ||
| 254 | }; | ||
| 255 | |||
| 256 | Ok(Html(template.render().unwrap_or_default())) | ||
| 257 | } | ||
| 258 | |||
| 259 | #[axum::debug_handler] | ||
| 260 | async fn edit_instance( | ||
| 261 | State(state): State<AppState>, | ||
| 262 | Path(id): Path<i64>, | ||
| 263 | CurrentUser(_user): CurrentUser, | ||
| 264 | Form(form): Form<EditInstanceForm>, | ||
| 265 | ) -> Result<Redirect, axum::response::Redirect> { | ||
| 266 | if form.memory_mb > state.config.max_memory_mb as i32 || form.memory_mb < 512 { | ||
| 267 | return Ok(Redirect::to(&format!("/instances/{id}/edit"))); | ||
| 268 | } | ||
| 269 | |||
| 270 | let pvp = form.pvp.as_deref() == Some("on"); | ||
| 271 | |||
| 272 | let is_active = { | ||
| 273 | let conn = state.db.conn(); | ||
| 274 | conn.execute( | ||
| 275 | "UPDATE instances SET difficulty = ?1, gamemode = ?2, max_players = ?3, | ||
| 276 | pvp = ?4, motd = ?5, memory_mb = ?6, updated_at = datetime('now') | ||
| 277 | WHERE id = ?7", | ||
| 278 | params![ | ||
| 279 | form.difficulty, | ||
| 280 | form.gamemode, | ||
| 281 | form.max_players, | ||
| 282 | pvp, | ||
| 283 | form.motd, | ||
| 284 | form.memory_mb, | ||
| 285 | id, | ||
| 286 | ], | ||
| 287 | ) | ||
| 288 | .map_err(|_| Redirect::to("/instances"))?; | ||
| 289 | |||
| 290 | conn.query_row( | ||
| 291 | "SELECT is_active FROM instances WHERE id = ?1", | ||
| 292 | params![id], | ||
| 293 | |row| row.get::<_, i32>(0).map(|v| v != 0), | ||
| 294 | ) | ||
| 295 | .unwrap_or(false) | ||
| 296 | }; | ||
| 297 | |||
| 298 | if is_active { | ||
| 299 | let _ = state.docker.stop_container().await; | ||
| 300 | } | ||
| 301 | |||
| 302 | Ok(Redirect::to("/instances")) | ||
| 303 | } | ||
| 304 | |||
| 305 | async fn delete_instance( | ||
| 306 | State(state): State<AppState>, | ||
| 307 | Path(id): Path<i64>, | ||
| 308 | CurrentUser(_user): CurrentUser, | ||
| 309 | ) -> Result<Redirect, axum::response::Redirect> { | ||
| 310 | let conn = state.db.conn(); | ||
| 311 | let is_active: bool = conn | ||
| 312 | .query_row( | ||
| 313 | "SELECT is_active FROM instances WHERE id = ?1", | ||
| 314 | params![id], | ||
| 315 | |row| row.get::<_, i32>(0).map(|v| v != 0), | ||
| 316 | ) | ||
| 317 | .unwrap_or(false); | ||
| 318 | |||
| 319 | if is_active { | ||
| 320 | drop(conn); | ||
| 321 | return Ok(Redirect::to("/instances")); | ||
| 322 | } | ||
| 323 | |||
| 324 | let name: Option<String> = conn | ||
| 325 | .query_row( | ||
| 326 | "SELECT name FROM instances WHERE id = ?1", | ||
| 327 | params![id], | ||
| 328 | |row| row.get(0), | ||
| 329 | ) | ||
| 330 | .ok(); | ||
| 331 | |||
| 332 | conn.execute("DELETE FROM instances WHERE id = ?1", params![id]) | ||
| 333 | .ok(); | ||
| 334 | drop(conn); | ||
| 335 | |||
| 336 | if let Some(name) = name { | ||
| 337 | let instance_dir = state.config.server_directory.join("instances").join(&name); | ||
| 338 | let _ = std::fs::remove_dir_all(&instance_dir); | ||
| 339 | } | ||
| 340 | |||
| 341 | Ok(Redirect::to("/instances")) | ||
| 342 | } | ||
| 343 | |||
| 344 | #[axum::debug_handler] | ||
| 345 | async fn activate_instance( | ||
| 346 | State(state): State<AppState>, | ||
| 347 | Path(id): Path<i64>, | ||
| 348 | CurrentUser(_user): CurrentUser, | ||
| 349 | ) -> Result<Redirect, axum::response::Redirect> { | ||
| 350 | // Stop any running container first (this is usually fast) | ||
| 351 | let _ = state.docker.stop_container().await; | ||
| 352 | |||
| 353 | // Update DB: deactivate all, activate the new one | ||
| 354 | let instance = { | ||
| 355 | let conn = state.db.conn(); | ||
| 356 | conn.execute("UPDATE instances SET is_active = 0 WHERE is_active = 1", []) | ||
| 357 | .ok(); | ||
| 358 | conn.execute( | ||
| 359 | "UPDATE instances SET is_active = 1, updated_at = datetime('now') WHERE id = ?1", | ||
| 360 | params![id], | ||
| 361 | ) | ||
| 362 | .ok(); | ||
| 363 | |||
| 364 | conn.query_row( | ||
| 365 | "SELECT id, name, version, server_type, world_source, world_seed, level_type, | ||
| 366 | has_modpack, memory_mb, difficulty, gamemode, max_players, pvp, motd, | ||
| 367 | rcon_password, is_active, created_at, updated_at | ||
| 368 | FROM instances WHERE id = ?1", | ||
| 369 | params![id], | ||
| 370 | |row| { | ||
| 371 | Ok(Instance { | ||
| 372 | id: row.get(0)?, | ||
| 373 | name: row.get(1)?, | ||
| 374 | version: row.get(2)?, | ||
| 375 | server_type: row.get(3)?, | ||
| 376 | world_source: row.get(4)?, | ||
| 377 | world_seed: row.get(5)?, | ||
| 378 | level_type: row.get(6)?, | ||
| 379 | has_modpack: row.get::<_, i32>(7)? != 0, | ||
| 380 | memory_mb: row.get(8)?, | ||
| 381 | difficulty: row.get(9)?, | ||
| 382 | gamemode: row.get(10)?, | ||
| 383 | max_players: row.get(11)?, | ||
| 384 | pvp: row.get::<_, i32>(12)? != 0, | ||
| 385 | motd: row.get(13)?, | ||
| 386 | rcon_password: row.get(14)?, | ||
| 387 | is_active: row.get::<_, i32>(15)? != 0, | ||
| 388 | created_at: row.get(16)?, | ||
| 389 | updated_at: row.get(17)?, | ||
| 390 | }) | ||
| 391 | }, | ||
| 392 | ) | ||
| 393 | .map_err(|_| Redirect::to("/instances"))? | ||
| 394 | }; | ||
| 395 | |||
| 396 | // Pull image + start container in background so the UI stays responsive | ||
| 397 | let docker = state.docker.clone(); | ||
| 398 | let db = state.db.clone(); | ||
| 399 | tokio::spawn(async move { | ||
| 400 | { | ||
| 401 | let conn = db.conn(); | ||
| 402 | conn.execute( | ||
| 403 | "UPDATE instances SET container_state = 'pulling' WHERE is_active = 1", | ||
| 404 | [], | ||
| 405 | ) | ||
| 406 | .ok(); | ||
| 407 | } | ||
| 408 | match docker.start_container(&instance).await { | ||
| 409 | Ok(()) => { | ||
| 410 | let conn = db.conn(); | ||
| 411 | conn.execute( | ||
| 412 | "UPDATE instances SET container_state = 'running' WHERE is_active = 1", | ||
| 413 | [], | ||
| 414 | ) | ||
| 415 | .ok(); | ||
| 416 | } | ||
| 417 | Err(e) => { | ||
| 418 | tracing::error!("Failed to start container: {e}"); | ||
| 419 | let conn = db.conn(); | ||
| 420 | conn.execute( | ||
| 421 | "UPDATE instances SET container_state = 'error' WHERE is_active = 1", | ||
| 422 | [], | ||
| 423 | ) | ||
| 424 | .ok(); | ||
| 425 | } | ||
| 426 | } | ||
| 427 | }); | ||
| 428 | |||
| 429 | Ok(Redirect::to("/")) | ||
| 430 | } | ||
| 431 | |||
| 432 | async fn deactivate_instance( | ||
| 433 | State(state): State<AppState>, | ||
| 434 | CurrentUser(_user): CurrentUser, | ||
| 435 | ) -> Result<Redirect, axum::response::Redirect> { | ||
| 436 | let _ = state.docker.stop_container().await; | ||
| 437 | |||
| 438 | let conn = state.db.conn(); | ||
| 439 | conn.execute( | ||
| 440 | "UPDATE instances SET is_active = 0, container_state = 'stopped' WHERE is_active = 1", | ||
| 441 | [], | ||
| 442 | ) | ||
| 443 | .ok(); | ||
| 444 | drop(conn); | ||
| 445 | |||
| 446 | Ok(Redirect::to("/")) | ||
| 447 | } | ||
| 448 | |||
| 449 | fn generate_rcon_password() -> String { | ||
| 450 | use rand::Rng; | ||
| 451 | let mut rng = rand::thread_rng(); | ||
| 452 | let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz0123456789".chars().collect(); | ||
| 453 | (0..16) | ||
| 454 | .map(|_| chars[rng.gen_range(0..chars.len())]) | ||
| 455 | .collect() | ||
| 456 | } | ||
| 457 | |||
| 458 | async fn fetch_minecraft_versions() -> Result<Vec<String>, String> { | ||
| 459 | let client = reqwest::Client::new(); | ||
| 460 | let manifest: serde_json::Value = client | ||
| 461 | .get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json") | ||
| 462 | .send() | ||
| 463 | .await | ||
| 464 | .map_err(|e| e.to_string())? | ||
| 465 | .json() | ||
| 466 | .await | ||
| 467 | .map_err(|e| e.to_string())?; | ||
| 468 | |||
| 469 | let versions: Vec<String> = manifest["versions"] | ||
| 470 | .as_array() | ||
| 471 | .ok_or("Invalid version manifest")? | ||
| 472 | .iter() | ||
| 473 | .filter_map(|v| { | ||
| 474 | let t = v["type"].as_str()?; | ||
| 475 | if t == "release" { | ||
| 476 | v["id"].as_str().map(String::from) | ||
| 477 | } else { | ||
| 478 | None | ||
| 479 | } | ||
| 480 | }) | ||
| 481 | .collect(); | ||
| 482 | |||
| 483 | Ok(versions) | ||
| 484 | } | ||
diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..1cb0e54 --- /dev/null +++ b/src/routes/mod.rs | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | pub mod accounts; | ||
| 2 | pub mod backups; | ||
| 3 | pub mod dashboard; | ||
| 4 | pub mod instances; | ||
| 5 | pub mod players; | ||
| 6 | |||
| 7 | use askama::Template; | ||
| 8 | use axum::{Router, response::Html}; | ||
| 9 | use tower_http::services::ServeDir; | ||
| 10 | |||
| 11 | use crate::auth::CurrentUser; | ||
| 12 | use crate::config::Config; | ||
| 13 | use crate::db::Database; | ||
| 14 | use crate::docker::DockerManager; | ||
| 15 | |||
| 16 | #[derive(Clone)] | ||
| 17 | pub struct AppState { | ||
| 18 | pub config: Config, | ||
| 19 | pub db: Database, | ||
| 20 | pub docker: DockerManager, | ||
| 21 | } | ||
| 22 | |||
| 23 | pub fn build_router(state: AppState) -> Router { | ||
| 24 | let auth_required = | ||
| 25 | axum::middleware::from_fn_with_state(state.clone(), crate::auth::require_auth); | ||
| 26 | |||
| 27 | let protected = Router::new() | ||
| 28 | .route("/change-password", axum::routing::get(change_password_page)) | ||
| 29 | .route( | ||
| 30 | "/change-password", | ||
| 31 | axum::routing::post(accounts::change_password), | ||
| 32 | ) | ||
| 33 | .merge(dashboard::router()) | ||
| 34 | .merge(instances::router()) | ||
| 35 | .merge(players::router()) | ||
| 36 | .merge(accounts::router()) | ||
| 37 | .merge(backups::router()) | ||
| 38 | .layer(auth_required); | ||
| 39 | |||
| 40 | Router::new() | ||
| 41 | .route("/login", axum::routing::get(login_page)) | ||
| 42 | .route("/login", axum::routing::post(crate::auth::authenticate)) | ||
| 43 | .route("/logout", axum::routing::post(crate::auth::logout)) | ||
| 44 | .merge(protected) | ||
| 45 | .nest_service("/static", ServeDir::new("static")) | ||
| 46 | .with_state(state) | ||
| 47 | } | ||
| 48 | |||
| 49 | #[derive(Template)] | ||
| 50 | #[template(path = "login.html")] | ||
| 51 | struct LoginPageTemplate; | ||
| 52 | |||
| 53 | async fn login_page() -> Html<String> { | ||
| 54 | let template = LoginPageTemplate; | ||
| 55 | Html(template.render().unwrap_or_default()) | ||
| 56 | } | ||
| 57 | |||
| 58 | #[derive(Template)] | ||
| 59 | #[template(path = "change_password.html")] | ||
| 60 | struct ChangePasswordPageTemplate { | ||
| 61 | username: String, | ||
| 62 | is_superuser: bool, | ||
| 63 | password_change_required: bool, | ||
| 64 | error: String, | ||
| 65 | } | ||
| 66 | |||
| 67 | async fn change_password_page(CurrentUser(account): CurrentUser) -> Html<String> { | ||
| 68 | let template = ChangePasswordPageTemplate { | ||
| 69 | username: account.username, | ||
| 70 | is_superuser: account.is_superuser, | ||
| 71 | password_change_required: account.password_change_required, | ||
| 72 | error: String::new(), | ||
| 73 | }; | ||
| 74 | Html(template.render().unwrap_or_default()) | ||
| 75 | } | ||
diff --git a/src/routes/players.rs b/src/routes/players.rs new file mode 100644 index 0000000..b98a39d --- /dev/null +++ b/src/routes/players.rs | |||
| @@ -0,0 +1,131 @@ | |||
| 1 | use askama::Template; | ||
| 2 | use axum::{ | ||
| 3 | Form, Router, | ||
| 4 | extract::State, | ||
| 5 | response::{Html, Redirect}, | ||
| 6 | routing::{get, post}, | ||
| 7 | }; | ||
| 8 | use serde::Deserialize; | ||
| 9 | |||
| 10 | use crate::auth::CurrentUser; | ||
| 11 | use crate::routes::AppState; | ||
| 12 | |||
| 13 | #[derive(Template)] | ||
| 14 | #[template(path = "players/manage.html")] | ||
| 15 | struct PlayerManagementTemplate { | ||
| 16 | username: String, | ||
| 17 | is_superuser: bool, | ||
| 18 | password_change_required: bool, | ||
| 19 | active_instance_name: Option<String>, | ||
| 20 | whitelist: Vec<String>, | ||
| 21 | ops: Vec<String>, | ||
| 22 | bans: Vec<String>, | ||
| 23 | message: Option<String>, | ||
| 24 | } | ||
| 25 | |||
| 26 | #[derive(Debug, Deserialize)] | ||
| 27 | pub struct PlayerAction { | ||
| 28 | pub username: String, | ||
| 29 | } | ||
| 30 | |||
| 31 | pub fn router() -> Router<AppState> { | ||
| 32 | Router::new() | ||
| 33 | .route("/players", get(manage_players)) | ||
| 34 | .route("/players/whitelist/add", post(add_whitelist)) | ||
| 35 | .route("/players/whitelist/remove", post(remove_whitelist)) | ||
| 36 | .route("/players/op/add", post(add_op)) | ||
| 37 | .route("/players/op/remove", post(remove_op)) | ||
| 38 | .route("/players/ban/add", post(add_ban)) | ||
| 39 | .route("/players/ban/remove", post(remove_ban)) | ||
| 40 | } | ||
| 41 | |||
| 42 | async fn manage_players( | ||
| 43 | State(state): State<AppState>, | ||
| 44 | CurrentUser(account): CurrentUser, | ||
| 45 | ) -> Result<Html<String>, axum::response::Redirect> { | ||
| 46 | let active = get_active_instance(&state.db); | ||
| 47 | |||
| 48 | let (instance_name, whitelist, ops, bans) = | ||
| 49 | if let Some((_id, inst_name, _rcon_pw, _container_state)) = active { | ||
| 50 | let data_dir = state | ||
| 51 | .config | ||
| 52 | .server_directory | ||
| 53 | .join("instances") | ||
| 54 | .join(&inst_name) | ||
| 55 | .join("data"); | ||
| 56 | |||
| 57 | let w = read_json_list(&data_dir.join("whitelist.json")); | ||
| 58 | let o = read_json_list(&data_dir.join("ops.json")); | ||
| 59 | let b = read_json_list(&data_dir.join("banned-players.json")); | ||
| 60 | (Some(inst_name), w, o, b) | ||
| 61 | } else { | ||
| 62 | (None, vec![], vec![], vec![]) | ||
| 63 | }; | ||
| 64 | |||
| 65 | let template = PlayerManagementTemplate { | ||
| 66 | username: account.username, | ||
| 67 | is_superuser: account.is_superuser, | ||
| 68 | password_change_required: account.password_change_required, | ||
| 69 | active_instance_name: instance_name, | ||
| 70 | whitelist, | ||
| 71 | ops, | ||
| 72 | bans, | ||
| 73 | message: None, | ||
| 74 | }; | ||
| 75 | |||
| 76 | Ok(Html(template.render().unwrap_or_default())) | ||
| 77 | } | ||
| 78 | |||
| 79 | fn read_json_list(path: &std::path::Path) -> Vec<String> { | ||
| 80 | if let Ok(contents) = std::fs::read_to_string(path) { | ||
| 81 | if let Ok(entries) = serde_json::from_str::<Vec<serde_json::Value>>(&contents) { | ||
| 82 | return entries | ||
| 83 | .iter() | ||
| 84 | .filter_map(|e| e["name"].as_str().map(String::from)) | ||
| 85 | .collect(); | ||
| 86 | } | ||
| 87 | } | ||
| 88 | vec![] | ||
| 89 | } | ||
| 90 | |||
| 91 | macro_rules! player_action { | ||
| 92 | ($name:ident, $command:expr) => { | ||
| 93 | async fn $name( | ||
| 94 | State(state): State<AppState>, | ||
| 95 | CurrentUser(_user): CurrentUser, | ||
| 96 | Form(form): Form<PlayerAction>, | ||
| 97 | ) -> Result<Redirect, axum::response::Redirect> { | ||
| 98 | if let Some((_, _, rcon_pw, _)) = get_active_instance(&state.db) { | ||
| 99 | let client = crate::rcon::RconClient::new(rcon_pw); | ||
| 100 | let cmd = format!($command, form.username); | ||
| 101 | match client.send_command_async(cmd.clone()).await { | ||
| 102 | Ok(resp) => tracing::info!("RCON '{}' -> {}", cmd, resp.trim()), | ||
| 103 | Err(e) => tracing::error!("RCON '{}' failed: {}", cmd, e), | ||
| 104 | } | ||
| 105 | } else { | ||
| 106 | tracing::warn!( | ||
| 107 | "Player action '{}' called but no active instance", | ||
| 108 | stringify!($name) | ||
| 109 | ); | ||
| 110 | } | ||
| 111 | Ok(Redirect::to("/players")) | ||
| 112 | } | ||
| 113 | }; | ||
| 114 | } | ||
| 115 | |||
| 116 | player_action!(add_whitelist, "whitelist add {}"); | ||
| 117 | player_action!(remove_whitelist, "whitelist remove {}"); | ||
| 118 | player_action!(add_op, "op {}"); | ||
| 119 | player_action!(remove_op, "deop {}"); | ||
| 120 | player_action!(add_ban, "ban {}"); | ||
| 121 | player_action!(remove_ban, "pardon {}"); | ||
| 122 | |||
| 123 | fn get_active_instance(db: &crate::db::Database) -> Option<(i64, String, String, String)> { | ||
| 124 | db.conn() | ||
| 125 | .query_row( | ||
| 126 | "SELECT id, name, rcon_password, container_state FROM instances WHERE is_active = 1", | ||
| 127 | [], | ||
| 128 | |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), | ||
| 129 | ) | ||
| 130 | .ok() | ||
| 131 | } | ||
diff --git a/static/htmx.min.js b/static/htmx.min.js new file mode 100644 index 0000000..59937d7 --- /dev/null +++ b/static/htmx.min.js | |||
| @@ -0,0 +1 @@ | |||
| var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e<t.length;e++){n.push(t[e])}}return n}function se(t,n){if(t){for(let e=0;e<t.length;e++){n(t[e])}}}function X(e){const t=e.getBoundingClientRect();const n=t.top;const r=t.bottom;return n<window.innerHeight&&r>=0}function le(e){return e.getRootNode({composed:true})===document}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){O(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function u(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return u(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(t,r,n){if(r.indexOf("global ")===0){return p(t,r.slice(7),true)}t=y(t);const o=[];{let t=0;let n=0;for(let e=0;e<r.length;e++){const l=r[e];if(l===","&&t===0){o.push(r.substring(n,e));n=e+1;continue}if(l==="<"){t++}else if(l==="/"&&e<r.length-1&&r[e+1]===">"){t--}}if(n<r.length){o.push(r.substring(n))}}const i=[];const s=[];while(o.length>0){const r=ge(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),ge(r.substr(8)))}else if(r.indexOf("find ")===0){e=u(f(t),ge(r.substr(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,ge(r.substr(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,ge(r.substr(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=m(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=f(m(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e<r.length;e++){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_PRECEDING){return o}}};var me=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=r.length-1;e>=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return u(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){O('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(o(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e<n.length;e++){if(t===n[e]){return true}}return false}function Oe(t,n){se(t.attributes,function(e){if(!n.hasAttribute(e.name)&&Ce(e.name)){t.removeAttribute(e.name)}});se(n.attributes,function(e){if(Ce(e.name)){t.setAttribute(e.name,e.value)}})}function Re(t,e){const n=Un(e);for(let e=0;e<n.length;e++){const r=n[e];try{if(r.isInlineSwap(t)){return true}}catch(e){O(e)}}return t==="outerHTML"}function He(e,o,i,t){t=t||ne();let n="#"+ee(o,"id");let s="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!he(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){he(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=u("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=u("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=u("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>");e=u("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ae(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ne(f(e));he(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function a(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n<e.length){t=(t<<5)-t+e.charCodeAt(n++)|0}return t}function Pe(t){let n=0;if(t.attributes){for(let e=0;e<t.attributes.length;e++){const r=t.attributes[e];if(r.value){n=Ie(r.name,n);n=Ie(r.value,n)}}}return n}function ke(t){const n=ie(t);if(n.onHandlers){for(let e=0;e<n.onHandlers.length;e++){const r=n.onHandlers[e];be(t,r.event,r.listener)}delete n.onHandlers}}function De(e){const t=ie(e);if(t.timeout){clearTimeout(t.timeout)}if(t.listenerInfos){se(t.listenerInfos,function(e){if(e.on){be(e.on,e.trigger,e.listener)}})}ke(e);se(Object.keys(t),function(e){if(e!=="firstInitCompleted")delete t[e]})}function b(e){he(e,"htmx:beforeCleanupElement");De(e);if(e.children){se(e.children,function(e){b(e)})}}function Me(t,e,n){if(t instanceof Element&&t.tagName==="BODY"){return Ve(t,e,n)}let r;const o=t.previousSibling;const i=c(t);if(!i){return}a(i,t,e,n);if(o==null){r=i.firstChild}else{r=o.nextSibling}n.elts=n.elts.filter(function(e){return e!==t});while(r&&r!==t){if(r instanceof Element){n.elts.push(r)}r=r.nextSibling}b(t);if(t instanceof Element){t.remove()}else{t.parentNode.removeChild(t)}}function Xe(e,t,n){return a(e,e.firstChild,t,n)}function Fe(e,t,n){return a(c(e),e,t,n)}function Be(e,t,n){return a(e,null,t,n)}function Ue(e,t,n){return a(c(e),e.nextSibling,t,n)}function je(e){b(e);const t=c(e);if(t){return t.removeChild(e)}}function Ve(e,t,n){const r=e.firstChild;a(e,r,t,n);if(r){while(r.nextSibling){b(r.nextSibling);e.removeChild(r.nextSibling)}b(r);e.removeChild(r)}}function _e(t,e,n,r,o){switch(t){case"none":return;case"outerHTML":Me(n,r,o);return;case"afterbegin":Xe(n,r,o);return;case"beforebegin":Fe(n,r,o);return;case"beforeend":Be(n,r,o);return;case"afterend":Ue(n,r,o);return;case"delete":je(n);return;default:var i=Un(e);for(let e=0;e<i.length;e++){const s=i[e];try{const l=s.handleSwap(t,n,r,o);if(l){if(Array.isArray(l)){for(let e=0;e<l.length;e++){const c=l[e];if(c.nodeType!==Node.TEXT_NODE&&c.nodeType!==Node.COMMENT_NODE){o.tasks.push(Ae(c))}}}return}}catch(e){O(e)}}if(t==="innerHTML"){Ve(n,r,o)}else{_e(Q.config.defaultSwapStyle,e,n,r,o)}}}function ze(e,n,r){var t=x(e,"[hx-swap-oob], [data-hx-swap-oob]");se(t,function(e){if(Q.config.allowNestedOobSwaps||e.parentElement===null){const t=te(e,"hx-swap-oob");if(t!=null){He(t,e,n,r)}}else{e.removeAttribute("hx-swap-oob");e.removeAttribute("data-hx-swap-oob")}});return t.length>0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t<u.length;t++){const a=u[t].split(":",2);let e=a[0].trim();if(e.indexOf("#")===0){e=e.substring(1)}const f=a[1]||"true";const h=n.querySelector("#"+e);if(h){He(f,h,l,i)}}}ze(n,l,i);se(x(n,"template"),function(e){if(e.content&&ze(e.content,l,i)){e.remove()}});if(o.select){const d=ne().createDocumentFragment();se(n.querySelectorAll(o.select),function(e){d.appendChild(e)});n=d}qe(n);_e(r.swapStyle,o.contextElement,e,n,l);Te()}if(s.elt&&!le(s.elt)&&ee(s.elt,"id")){const g=document.getElementById(ee(s.elt,"id"));const p={preventScroll:r.focusScroll!==undefined?!r.focusScroll:!Q.config.defaultFocusScroll};if(g){if(s.start&&g.setSelectionRange){try{g.setSelectionRange(s.start,s.end)}catch(e){}}g.focus(p)}}e.classList.remove(Q.config.swappingClass);se(l.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}he(e,"htmx:afterSwap",o.eventInfo)});if(o.afterSwapCallback){o.afterSwapCallback()}if(!r.ignoreTitle){kn(l.title)}const c=function(){se(l.tasks,function(e){e.call()});se(l.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}he(e,"htmx:afterSettle",o.eventInfo)});if(o.anchor){const e=ue(y("#"+o.anchor));if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}yn(l.elts,r);if(o.afterSettleCallback){o.afterSettleCallback()}};if(r.settleDelay>0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}he(n,i,e)}}}else{const s=r.split(",");for(let e=0;e<s.length;e++){he(n,s[e].trim(),[])}}}const Ke=/\s/;const v=/[\s,]/;const Ge=/[_$a-zA-Z]/;const We=/[_$a-zA-Z0-9]/;const Ze=['"',"'","/"];const w=/[^\s]/;const Ye=/[{(]/;const Qe=/[})]/;function et(e){const t=[];let n=0;while(n<e.length){if(Ge.exec(e.charAt(n))){var r=n;while(We.exec(e.charAt(n+1))){n++}t.push(e.substring(r,n+1))}else if(Ze.indexOf(e.charAt(n))!==-1){const o=e.charAt(n);var r=n;n++;while(n<e.length&&e.charAt(n)!==o){if(e.charAt(n)==="\\"){n++}n++}t.push(e.substring(r,n+1))}else{const i=e.charAt(n);t.push(i)}n++}return t}function tt(e,t,n){return Ge.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function nt(r,o,i){if(o[0]==="["){o.shift();let e=1;let t=" return (function("+i+"){ return (";let n=null;while(o.length>0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function C(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=C(e,Qe).trim();e.shift()}else{t=C(e,v)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{C(o,w);const l=o.length;const c=C(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};C(o,w);u.pollInterval=d(C(o,/[,\[\s]/));C(o,w);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}C(o,w);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=d(C(o,v))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=C(o,v);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=d(C(o,v))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=C(o,v)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=C(o,v)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=ne().location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){b(n);return}de(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(h(n,'input[type="submit"], button')&&(h(n,"[form]")||g(n,"form")!==null)){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(a||ht(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){he(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){he(l,"htmx:trigger");c(l,e)},u.delay)}else{he(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){he(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){he(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;he(e,"htmx:trigger");t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(r,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){b(n);return}de(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e<t.length;e++){const n=t[e];if(n.isIntersecting){he(r,"intersect");break}}},o);i.observe(ue(r));pt(ue(r),n,t,e)}else if(!t.firstInitCompleted&&e.trigger==="load"){if(!gt(e,r,Mt("load",{elt:r}))){vt(ue(r),n,t,e.delay)}}else if(e.pollInterval>0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e<n.length;e++){const r=n[e].name;if(l(r,"hx-on:")||l(r,"data-hx-on:")||l(r,"hx-on-")||l(r,"data-hx-on-")){return true}}return false}const Ct=(new XPathEvaluator).createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or'+' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function Ot(e,t){if(Et(e)){t.push(ue(e))}const n=Ct.evaluate(e);let r=null;while(r=n.iterateNext())t.push(ue(r))}function Rt(e){const t=[];if(e instanceof DocumentFragment){for(const n of e.childNodes){Ot(n,t)}}else{Ot(e,t)}return t}function Ht(e){if(e.querySelectorAll){const n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";const r=[];for(const i in Mn){const s=Mn[i];if(s.getSelectors){var t=s.getSelectors();if(t){r.push(t)}}}const o=e.querySelectorAll(H+n+", form, [type='submit'],"+" [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(e=>", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Nt(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;e<t.attributes.length;e++){const n=t.attributes[e].name;const r=t.attributes[e].value;if(l(n,"hx-on")||l(n,"data-hx-on")){const o=n.indexOf("-on")+3;const i=n.slice(o,o+1);if(i==="-"||i===":"){let e=n.slice(o+1);if(l(e,":")){e="htmx"+e}else if(l(e,"-")){e="htmx:"+e.slice(1)}else if(l(e,"htmx-")){e="htmx:"+e.slice(5)}Nt(t,e,r)}}}}function Pt(t){if(g(t,Q.config.disableSelector)){b(t);return}const n=ie(t);const e=Pe(t);if(n.initHash!==e){De(t);n.initHash=e;he(t,"htmx:beforeProcessNode");const r=st(t);const o=wt(t,n,r);if(!o){if(re(t,"hx-boost")==="true"){ft(t,n,r)}else if(s(t,"hx-trigger")){r.forEach(function(e){St(t,e,n,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&s(t,"form")){At(t)}n.firstInitCompleted=true;he(t,"htmx:afterProcessNode")}}function kt(e){e=y(e);if(g(e,Q.config.disableSelector)){b(e);return}Pt(e);se(Ht(e),function(e){Pt(e)});se(Rt(e),It)}function Dt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Mt(e,t){let n;if(window.CustomEvent&&typeof window.CustomEvent==="function"){n=new CustomEvent(e,{bubbles:true,cancelable:true,composed:true,detail:t})}else{n=ne().createEvent("CustomEvent");n.initCustomEvent(e,true,true,t)}return n}function fe(e,t,n){he(e,t,ce({error:t},n))}function Xt(e){return e==="htmx:afterProcessNode"}function Ft(e,t){se(Un(e),function(e){try{t(e)}catch(e){O(e)}})}function O(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function he(e,t,n){e=y(e);if(n==null){n={}}n.elt=e;const r=Mt(t,n);if(Q.logger&&!Xt(t)){Q.logger(e,t,n)}if(n.error){O(n.error);he(e,"htmx:error",{errorInfo:n})}let o=e.dispatchEvent(r);const i=Dt(t);if(o&&i!==t){const s=Mt(i,r.detail);o=o&&e.dispatchEvent(s)}Ft(ue(e),function(e){o=o&&(e.onEvent(t,r)!==false&&!r.defaultPrevented)});return o}let Bt=location.pathname+location.search;function Ut(){const e=ne().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||ne().body}function jt(t,e){if(!B()){return}const n=_t(e);const r=ne().title;const o=window.scrollY;if(Q.config.historyCacheSize<=0){localStorage.removeItem("htmx-history-cache");return}t=U(t);const i=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<i.length;e++){if(i[e].url===t){i.splice(e,1);break}}const s={url:t,content:n,title:r,scroll:o};he(ne().body,"htmx:historyItemCreated",{item:s,cache:i});i.push(s);while(i.length>Q.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<n.length;e++){if(n[e].url===t){return n[e]}}return null}function _t(e){const t=Q.config.requestClass;const n=e.cloneNode(true);se(x(n,"."+t),function(e){G(e,t)});se(x(n,"[data-disabled-by-htmx]"),function(e){e.removeAttribute("disabled")});return n.innerHTML}function zt(){const e=Ut();const t=Bt||location.pathname+location.search;let n;try{n=ne().querySelector('[hx-history="false" i],[data-hx-history="false" i]')}catch(e){n=ne().querySelector('[hx-history="false"],[data-hx-history="false"]')}if(!n){he(ne().body,"htmx:beforeHistorySave",{path:t,historyElt:e});jt(t,e)}if(Q.config.historyEnabled)history.replaceState({htmx:true},ne().title,window.location.href)}function $t(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(Y(e,"&")||Y(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}Bt=e}function Jt(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);Bt=e}function Kt(e){se(e,function(e){e.call(undefined)})}function Gt(o){const e=new XMLHttpRequest;const i={path:o,xhr:e};he(ne().body,"htmx:historyCacheMiss",i);e.open("GET",o,true);e.setRequestHeader("HX-Request","true");e.setRequestHeader("HX-History-Restore-Request","true");e.setRequestHeader("HX-Current-URL",ne().location.href);e.onload=function(){if(this.status>=200&&this.status<400){he(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;he(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;he(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;e<t.length;e++){const r=t[e];if(r.isSameNode(n)){return true}}return false}function tn(e){const t=e;if(t.name===""||t.name==null||t.disabled||g(t,"fieldset[disabled]")){return false}if(t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"){return false}if(t.type==="checkbox"||t.type==="radio"){return t.checked}return true}function nn(t,e,n){if(t!=null&&e!=null){if(Array.isArray(e)){e.forEach(function(e){n.append(t,e)})}else{n.append(t,e)}}}function rn(t,n,r){if(t!=null&&n!=null){let e=r.getAll(t);if(Array.isArray(n)){e=e.filter(e=>n.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){he(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});he(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!h(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:An(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function dn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!dn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e<s.length;e++){const l=s[e];if(l.indexOf("swap:")===0){r.swapDelay=d(l.slice(5))}else if(l.indexOf("settle:")===0){r.settleDelay=d(l.slice(7))}else if(l.indexOf("transition:")===0){r.transition=l.slice(11)==="true"}else if(l.indexOf("ignoreTitle:")===0){r.ignoreTitle=l.slice(12)==="true"}else if(l.indexOf("scroll:")===0){const c=l.slice(7);var o=c.split(":");const u=o.pop();var i=o.length>0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.slice(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{O("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function R(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return de(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||r.source&&!e&&!y(r.source)){e=ve}return de(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return de(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return he(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}else{return e[t]}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function de(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const A=ee(a,"formmethod");if(A!=null){if(A.toLowerCase()!=="dialog"){t=A}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return de(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(he(r,"htmx:confirm",G)===false){oe(s);return e}}let h=r;let d=re(r,"hx-sync");let g=null;let F=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ue(ae(r,I))}d=(N[1]||"drop").trim();u=ie(h);if(d==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(d==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(d==="replace"){he(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){he(h,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){de(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!he(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=hn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:An(w),unfilteredFormData:v,unfilteredParameters:An(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!he(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){he(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}he(r,"htmx:afterRequest",H);he(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){he(e,"htmx:afterRequest",H);he(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!he(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){he(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});he(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function Nn(e,t){const n=t.xhr;let r=null;let o=null;if(R(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(R(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(R(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t<Q.config.responseHandling.length;t++){var n=Q.config.responseHandling[t];if(In(n,e.status)){return n}}return{swap:false}}function kn(e){if(e){const t=u("title");if(t){t.innerHTML=e}else{window.document.title=e}}}function Dn(o,i){const s=i.xhr;let l=i.target;const e=i.etc;const c=i.select;if(!he(o,"htmx:beforeOnLoad",i))return;if(R(s,/HX-Trigger:/i)){Je(s,"HX-Trigger",o)}if(R(s,/HX-Location:/i)){zt();let e=s.getResponseHeader("HX-Location");var t;if(e.indexOf("{")===0){t=S(e);e=t.path;delete t.path}Rn("get",e,t).then(function(){$t(e)});return}const n=R(s,/HX-Refresh:/i)&&s.getResponseHeader("HX-Refresh")==="true";if(R(s,/HX-Redirect:/i)){i.keepIndicators=true;location.href=s.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){i.keepIndicators=true;location.reload();return}if(R(s,/HX-Retarget:/i)){if(s.getResponseHeader("HX-Retarget")==="this"){i.target=o}else{i.target=ue(ae(o,s.getResponseHeader("HX-Retarget")))}}const u=Nn(o,i);const r=Pn(s);const a=r.swap;let f=!!r.error;let h=Q.config.ignoreTitle||r.ignoreTitle;let d=r.select;if(r.target){i.target=ue(ae(o,r.target))}var g=e.swapOverride;if(g==null&&r.swapOverride){g=r.swapOverride}if(R(s,/HX-Retarget:/i)){if(s.getResponseHeader("HX-Retarget")==="this"){i.target=o}else{i.target=ue(ae(o,s.getResponseHeader("HX-Retarget")))}}if(R(s,/HX-Reswap:/i)){g=s.getResponseHeader("HX-Reswap")}var p=s.response;var m=ce({shouldSwap:a,serverResponse:p,isError:f,ignoreTitle:h,selectOverride:d,swapOverride:g},i);if(r.event&&!he(l,r.event,m))return;if(!he(l,"htmx:beforeSwap",m))return;l=m.target;p=m.serverResponse;f=m.isError;h=m.ignoreTitle;d=m.selectOverride;g=m.swapOverride;i.target=l;i.failed=f;i.successful=!f;if(m.shouldSwap){if(s.status===286){lt(o)}Ft(o,function(e){p=e.transformResponse(p,s,o)});if(u.type){zt()}var x=gn(o,g);if(!x.hasOwnProperty("ignoreTitle")){x.ignoreTitle=h}l.classList.add(Q.config.swappingClass);let n=null;let r=null;if(c){d=c}if(R(s,/HX-Reselect:/i)){d=s.getResponseHeader("HX-Reselect")}const y=re(o,"hx-select-oob");const b=re(o,"hx-select");let e=function(){try{if(u.type){he(ne().body,"htmx:beforeHistoryUpdate",ce({history:u},i));if(u.type==="push"){$t(u.path);he(ne().body,"htmx:pushedIntoHistory",{path:u.path})}else{Jt(u.path);he(ne().body,"htmx:replacedInHistory",{path:u.path})}}$e(l,p,x,{select:d||b,selectOOB:y,eventInfo:i,anchor:i.pathInfo.anchor,contextElement:o,afterSwapCallback:function(){if(R(s,/HX-Trigger-After-Swap:/i)){let e=o;if(!le(o)){e=ne().body}Je(s,"HX-Trigger-After-Swap",e)}},afterSettleCallback:function(){if(R(s,/HX-Trigger-After-Settle:/i)){let e=o;if(!le(o)){e=ne().body}Je(s,"HX-Trigger-After-Settle",e)}oe(n)}})}catch(e){fe(o,"htmx:swapError",i);oe(r);throw e}};let t=Q.config.globalViewTransitions;if(x.hasOwnProperty("transition")){t=x.transition}if(t&&he(o,"htmx:beforeTransition",i)&&typeof Promise!=="undefined"&&document.startViewTransition){const v=new Promise(function(e,t){n=e;r=t});const w=e;e=function(){document.startViewTransition(function(){w();return v})}}if(x.swapDelay>0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend","<style"+e+"> ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} </style>")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){he(e,"htmx:restored",{document:ne(),triggerEvent:he})})}else{if(n){n(e)}}};E().setTimeout(function(){he(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file | |||
diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..2d095a4 --- /dev/null +++ b/static/style.css | |||
| @@ -0,0 +1,424 @@ | |||
| 1 | *, | ||
| 2 | *::before, | ||
| 3 | *::after { | ||
| 4 | box-sizing: border-box; | ||
| 5 | margin: 0; | ||
| 6 | padding: 0; | ||
| 7 | } | ||
| 8 | |||
| 9 | :root { | ||
| 10 | --bg: #1a1b1e; | ||
| 11 | --bg-card: #25262b; | ||
| 12 | --bg-input: #2c2e33; | ||
| 13 | --fg: #c1c2c5; | ||
| 14 | --fg-muted: #909296; | ||
| 15 | --accent: #4c9aff; | ||
| 16 | --ok: #51cf66; | ||
| 17 | --warn: #fcc419; | ||
| 18 | --err: #ff6b6b; | ||
| 19 | --border: #373a40; | ||
| 20 | --radius: 8px; | ||
| 21 | } | ||
| 22 | |||
| 23 | body { | ||
| 24 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | ||
| 25 | background: var(--bg); | ||
| 26 | color: var(--fg); | ||
| 27 | line-height: 1.5; | ||
| 28 | min-height: 100vh; | ||
| 29 | } | ||
| 30 | |||
| 31 | /* Navigation */ | ||
| 32 | nav { | ||
| 33 | display: flex; | ||
| 34 | align-items: center; | ||
| 35 | justify-content: space-between; | ||
| 36 | padding: 0.75rem 1.5rem; | ||
| 37 | background: var(--bg-card); | ||
| 38 | border-bottom: 1px solid var(--border); | ||
| 39 | flex-wrap: wrap; | ||
| 40 | gap: 0.5rem; | ||
| 41 | } | ||
| 42 | |||
| 43 | nav .brand { | ||
| 44 | font-weight: 700; | ||
| 45 | font-size: 1.1rem; | ||
| 46 | color: var(--accent); | ||
| 47 | } | ||
| 48 | |||
| 49 | nav .nav-links { | ||
| 50 | display: flex; | ||
| 51 | align-items: center; | ||
| 52 | gap: 1rem; | ||
| 53 | flex-wrap: wrap; | ||
| 54 | } | ||
| 55 | |||
| 56 | nav a { | ||
| 57 | color: var(--fg); | ||
| 58 | text-decoration: none; | ||
| 59 | font-size: 0.9rem; | ||
| 60 | } | ||
| 61 | |||
| 62 | nav a:hover { | ||
| 63 | color: var(--accent); | ||
| 64 | } | ||
| 65 | |||
| 66 | nav .nav-user { | ||
| 67 | color: var(--fg-muted); | ||
| 68 | font-size: 0.85rem; | ||
| 69 | } | ||
| 70 | |||
| 71 | /* Buttons */ | ||
| 72 | .btn, button { | ||
| 73 | display: inline-block; | ||
| 74 | padding: 0.5rem 1rem; | ||
| 75 | border: 1px solid var(--border); | ||
| 76 | border-radius: var(--radius); | ||
| 77 | background: var(--bg-input); | ||
| 78 | color: var(--fg); | ||
| 79 | cursor: pointer; | ||
| 80 | font-size: 0.9rem; | ||
| 81 | text-decoration: none; | ||
| 82 | transition: background 0.15s; | ||
| 83 | } | ||
| 84 | |||
| 85 | .btn:hover, button:hover { | ||
| 86 | background: var(--border); | ||
| 87 | } | ||
| 88 | |||
| 89 | .btn-ok, button.btn-ok { | ||
| 90 | background: var(--ok); | ||
| 91 | color: #000; | ||
| 92 | border-color: var(--ok); | ||
| 93 | } | ||
| 94 | |||
| 95 | .btn-ok:hover { | ||
| 96 | opacity: 0.85; | ||
| 97 | } | ||
| 98 | |||
| 99 | .btn-danger, button.btn-danger { | ||
| 100 | background: var(--err); | ||
| 101 | color: #000; | ||
| 102 | border-color: var(--err); | ||
| 103 | } | ||
| 104 | |||
| 105 | .btn-danger:hover { | ||
| 106 | opacity: 0.85; | ||
| 107 | } | ||
| 108 | |||
| 109 | .btn-sm { | ||
| 110 | padding: 0.25rem 0.6rem; | ||
| 111 | font-size: 0.8rem; | ||
| 112 | } | ||
| 113 | |||
| 114 | .btn-link { | ||
| 115 | background: none; | ||
| 116 | border: none; | ||
| 117 | color: var(--fg-muted); | ||
| 118 | cursor: pointer; | ||
| 119 | padding: 0; | ||
| 120 | } | ||
| 121 | |||
| 122 | .btn-link:hover { | ||
| 123 | color: var(--err); | ||
| 124 | } | ||
| 125 | |||
| 126 | /* Cards */ | ||
| 127 | .card { | ||
| 128 | background: var(--bg-card); | ||
| 129 | border: 1px solid var(--border); | ||
| 130 | border-radius: var(--radius); | ||
| 131 | padding: 1.25rem; | ||
| 132 | } | ||
| 133 | |||
| 134 | .card-ok { | ||
| 135 | border-color: var(--ok); | ||
| 136 | } | ||
| 137 | |||
| 138 | .card-err { | ||
| 139 | border-color: var(--err); | ||
| 140 | } | ||
| 141 | |||
| 142 | .card-muted { | ||
| 143 | opacity: 0.7; | ||
| 144 | } | ||
| 145 | |||
| 146 | .card-active { | ||
| 147 | border-color: var(--accent); | ||
| 148 | position: relative; | ||
| 149 | } | ||
| 150 | |||
| 151 | .active-badge { | ||
| 152 | position: absolute; | ||
| 153 | top: -8px; | ||
| 154 | right: -8px; | ||
| 155 | background: var(--accent); | ||
| 156 | color: #000; | ||
| 157 | font-size: 0.7rem; | ||
| 158 | font-weight: 700; | ||
| 159 | padding: 0.15rem 0.5rem; | ||
| 160 | border-radius: 100px; | ||
| 161 | } | ||
| 162 | |||
| 163 | /* Forms */ | ||
| 164 | form.form-col { | ||
| 165 | display: flex; | ||
| 166 | flex-direction: column; | ||
| 167 | gap: 1rem; | ||
| 168 | } | ||
| 169 | |||
| 170 | form.form-col:not(.form-wide) { | ||
| 171 | max-width: 600px; | ||
| 172 | } | ||
| 173 | |||
| 174 | form.form-row { | ||
| 175 | display: flex; | ||
| 176 | align-items: center; | ||
| 177 | gap: 0.5rem; | ||
| 178 | } | ||
| 179 | |||
| 180 | label { | ||
| 181 | display: flex; | ||
| 182 | flex-direction: column; | ||
| 183 | gap: 0.25rem; | ||
| 184 | font-size: 0.9rem; | ||
| 185 | color: var(--fg-muted); | ||
| 186 | } | ||
| 187 | |||
| 188 | label:has(input[type="radio"]), | ||
| 189 | label:has(input[type="checkbox"]) { | ||
| 190 | flex-direction: row; | ||
| 191 | align-items: center; | ||
| 192 | gap: 0.5rem; | ||
| 193 | color: var(--fg); | ||
| 194 | } | ||
| 195 | |||
| 196 | input, select, textarea { | ||
| 197 | padding: 0.5rem 0.75rem; | ||
| 198 | border: 1px solid var(--border); | ||
| 199 | border-radius: var(--radius); | ||
| 200 | background: var(--bg-input); | ||
| 201 | color: var(--fg); | ||
| 202 | font-size: 0.9rem; | ||
| 203 | } | ||
| 204 | |||
| 205 | input:focus, select:focus { | ||
| 206 | outline: none; | ||
| 207 | border-color: var(--accent); | ||
| 208 | } | ||
| 209 | |||
| 210 | fieldset { | ||
| 211 | border: 1px solid var(--border); | ||
| 212 | border-radius: var(--radius); | ||
| 213 | padding: 1rem; | ||
| 214 | } | ||
| 215 | |||
| 216 | legend { | ||
| 217 | font-weight: 600; | ||
| 218 | padding: 0 0.5rem; | ||
| 219 | color: var(--fg-muted); | ||
| 220 | font-size: 0.85rem; | ||
| 221 | } | ||
| 222 | |||
| 223 | small { | ||
| 224 | color: var(--fg-muted); | ||
| 225 | font-size: 0.8rem; | ||
| 226 | } | ||
| 227 | |||
| 228 | /* Layout */ | ||
| 229 | main { | ||
| 230 | max-width: 1000px; | ||
| 231 | margin: 0 auto; | ||
| 232 | padding: 1.5rem; | ||
| 233 | } | ||
| 234 | |||
| 235 | h1, h2, h3 { | ||
| 236 | color: var(--fg); | ||
| 237 | margin-bottom: 0.75rem; | ||
| 238 | } | ||
| 239 | |||
| 240 | .page-header { | ||
| 241 | display: flex; | ||
| 242 | align-items: center; | ||
| 243 | justify-content: space-between; | ||
| 244 | margin-bottom: 1.5rem; | ||
| 245 | flex-wrap: wrap; | ||
| 246 | gap: 1rem; | ||
| 247 | } | ||
| 248 | |||
| 249 | .instance-grid { | ||
| 250 | display: grid; | ||
| 251 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||
| 252 | gap: 1rem; | ||
| 253 | } | ||
| 254 | |||
| 255 | .tab-row { | ||
| 256 | display: grid; | ||
| 257 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | ||
| 258 | gap: 1rem; | ||
| 259 | } | ||
| 260 | |||
| 261 | /* Status badges */ | ||
| 262 | .status-badge { | ||
| 263 | display: inline-block; | ||
| 264 | padding: 0.25rem 0.75rem; | ||
| 265 | border-radius: 100px; | ||
| 266 | font-size: 0.85rem; | ||
| 267 | font-weight: 600; | ||
| 268 | } | ||
| 269 | |||
| 270 | .status-running { | ||
| 271 | background: var(--ok); | ||
| 272 | color: #000; | ||
| 273 | } | ||
| 274 | |||
| 275 | .status-starting { | ||
| 276 | background: var(--warn); | ||
| 277 | color: #000; | ||
| 278 | } | ||
| 279 | |||
| 280 | .status-stopped { | ||
| 281 | background: var(--fg-muted); | ||
| 282 | color: #000; | ||
| 283 | } | ||
| 284 | |||
| 285 | .status-error, .status-not_found { | ||
| 286 | background: var(--err); | ||
| 287 | color: #000; | ||
| 288 | } | ||
| 289 | |||
| 290 | .status-row { | ||
| 291 | display: flex; | ||
| 292 | align-items: center; | ||
| 293 | gap: 1rem; | ||
| 294 | margin: 0.75rem 0; | ||
| 295 | } | ||
| 296 | |||
| 297 | /* Tags */ | ||
| 298 | .tag { | ||
| 299 | display: inline-block; | ||
| 300 | padding: 0.1rem 0.5rem; | ||
| 301 | border-radius: 100px; | ||
| 302 | font-size: 0.75rem; | ||
| 303 | background: var(--border); | ||
| 304 | color: var(--fg-muted); | ||
| 305 | } | ||
| 306 | |||
| 307 | .tag-admin { | ||
| 308 | background: var(--warn); | ||
| 309 | color: #000; | ||
| 310 | } | ||
| 311 | |||
| 312 | /* Instance meta */ | ||
| 313 | .instance-meta, .meta { | ||
| 314 | display: flex; | ||
| 315 | gap: 0.75rem; | ||
| 316 | flex-wrap: wrap; | ||
| 317 | font-size: 0.85rem; | ||
| 318 | color: var(--fg-muted); | ||
| 319 | margin-bottom: 0.5rem; | ||
| 320 | } | ||
| 321 | |||
| 322 | .actions { | ||
| 323 | display: flex; | ||
| 324 | gap: 0.5rem; | ||
| 325 | flex-wrap: wrap; | ||
| 326 | margin-top: 0.75rem; | ||
| 327 | } | ||
| 328 | |||
| 329 | .players { | ||
| 330 | font-size: 0.9rem; | ||
| 331 | color: var(--accent); | ||
| 332 | } | ||
| 333 | |||
| 334 | /* Player list */ | ||
| 335 | .player-list { | ||
| 336 | list-style: none; | ||
| 337 | margin-top: 0.5rem; | ||
| 338 | } | ||
| 339 | |||
| 340 | .player-list li { | ||
| 341 | display: flex; | ||
| 342 | align-items: center; | ||
| 343 | justify-content: space-between; | ||
| 344 | padding: 0.4rem 0; | ||
| 345 | border-bottom: 1px solid var(--border); | ||
| 346 | font-size: 0.9rem; | ||
| 347 | } | ||
| 348 | |||
| 349 | /* Table */ | ||
| 350 | .table { | ||
| 351 | width: 100%; | ||
| 352 | border-collapse: collapse; | ||
| 353 | font-size: 0.9rem; | ||
| 354 | } | ||
| 355 | |||
| 356 | .table th, .table td { | ||
| 357 | text-align: left; | ||
| 358 | padding: 0.5rem; | ||
| 359 | border-bottom: 1px solid var(--border); | ||
| 360 | } | ||
| 361 | |||
| 362 | .table th { | ||
| 363 | color: var(--fg-muted); | ||
| 364 | font-weight: 600; | ||
| 365 | } | ||
| 366 | |||
| 367 | /* Flash messages */ | ||
| 368 | .flash { | ||
| 369 | padding: 0.75rem 1rem; | ||
| 370 | border-radius: var(--radius); | ||
| 371 | margin-bottom: 1rem; | ||
| 372 | font-size: 0.9rem; | ||
| 373 | } | ||
| 374 | |||
| 375 | .flash-err { | ||
| 376 | background: color-mix(in srgb, var(--err) 15%, transparent); | ||
| 377 | border: 1px solid var(--err); | ||
| 378 | color: var(--err); | ||
| 379 | } | ||
| 380 | |||
| 381 | .flash-ok { | ||
| 382 | background: color-mix(in srgb, var(--ok) 15%, transparent); | ||
| 383 | border: 1px solid var(--ok); | ||
| 384 | color: var(--ok); | ||
| 385 | } | ||
| 386 | |||
| 387 | .flash-warn { | ||
| 388 | background: color-mix(in srgb, var(--warn) 15%, transparent); | ||
| 389 | border: 1px solid var(--warn); | ||
| 390 | color: var(--warn); | ||
| 391 | } | ||
| 392 | |||
| 393 | /* Login page */ | ||
| 394 | .login-page { | ||
| 395 | display: flex; | ||
| 396 | flex-direction: column; | ||
| 397 | align-items: center; | ||
| 398 | justify-content: center; | ||
| 399 | min-height: 100vh; | ||
| 400 | max-width: 380px; | ||
| 401 | } | ||
| 402 | |||
| 403 | .login-form { | ||
| 404 | width: 100%; | ||
| 405 | display: flex; | ||
| 406 | flex-direction: column; | ||
| 407 | gap: 1rem; | ||
| 408 | } | ||
| 409 | |||
| 410 | .login-form button { | ||
| 411 | margin-top: 0.5rem; | ||
| 412 | } | ||
| 413 | |||
| 414 | /* Responsive */ | ||
| 415 | @media (max-width: 600px) { | ||
| 416 | nav { | ||
| 417 | flex-direction: column; | ||
| 418 | align-items: flex-start; | ||
| 419 | } | ||
| 420 | |||
| 421 | .instance-grid { | ||
| 422 | grid-template-columns: 1fr; | ||
| 423 | } | ||
| 424 | } | ||
diff --git a/templates/accounts/manage.html b/templates/accounts/manage.html new file mode 100644 index 0000000..e7c9b2f --- /dev/null +++ b/templates/accounts/manage.html | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | {% block content %} | ||
| 3 | <h1>Account Management</h1> | ||
| 4 | |||
| 5 | {% if let Some(msg) = message %} | ||
| 6 | <div class="flash flash-ok">{{ msg }}</div> | ||
| 7 | {% endif %} | ||
| 8 | |||
| 9 | <div class="card"> | ||
| 10 | <h3>Change My Password</h3> | ||
| 11 | <form action="/accounts/change-own-password" method="post" class="form-row"> | ||
| 12 | <input type="password" name="current_password" placeholder="Current password" required> | ||
| 13 | <input type="password" name="new_password" placeholder="New password" required minlength="4"> | ||
| 14 | <button type="submit" class="btn-ok">Update</button> | ||
| 15 | </form> | ||
| 16 | </div> | ||
| 17 | |||
| 18 | <div class="card"> | ||
| 19 | <h3>Create Account</h3> | ||
| 20 | <form action="/accounts/create" method="post" class="form-row"> | ||
| 21 | <input type="text" name="username" placeholder="Username" required> | ||
| 22 | <input type="password" name="password" placeholder="Password" required> | ||
| 23 | <label> | ||
| 24 | <input type="checkbox" name="is_superuser"> Superuser | ||
| 25 | </label> | ||
| 26 | <button type="submit" class="btn-ok">Create</button> | ||
| 27 | </form> | ||
| 28 | </div> | ||
| 29 | |||
| 30 | <div class="card"> | ||
| 31 | <h3>Existing Accounts</h3> | ||
| 32 | <table class="table"> | ||
| 33 | <thead> | ||
| 34 | <tr> | ||
| 35 | <th>Username</th> | ||
| 36 | <th>Role</th> | ||
| 37 | <th>Actions</th> | ||
| 38 | </tr> | ||
| 39 | </thead> | ||
| 40 | <tbody> | ||
| 41 | {% for acct in accounts %} | ||
| 42 | <tr> | ||
| 43 | <td>{{ acct.username }}</td> | ||
| 44 | <td> | ||
| 45 | {% if acct.is_superuser %} | ||
| 46 | <span class="tag tag-admin">Superuser</span> | ||
| 47 | {% else %} | ||
| 48 | <span class="tag">User</span> | ||
| 49 | {% endif %} | ||
| 50 | </td> | ||
| 51 | <td> | ||
| 52 | <div class="actions" style="gap:0.25rem"> | ||
| 53 | <form action="/accounts/set-password" method="post" style="display:inline"> | ||
| 54 | <input type="hidden" name="id" value="{{ acct.id }}"> | ||
| 55 | <input type="password" name="new_password" placeholder="New password" required minlength="4" style="width:140px;padding:0.25rem 0.5rem;font-size:0.8rem"> | ||
| 56 | <button type="submit" class="btn-sm">Set</button> | ||
| 57 | </form> | ||
| 58 | <form action="/accounts/delete" method="post" style="display:inline" | ||
| 59 | onsubmit="return confirm('Delete account {{ acct.username }}?')"> | ||
| 60 | <input type="hidden" name="id" value="{{ acct.id }}"> | ||
| 61 | <button type="submit" class="btn-sm btn-danger">Delete</button> | ||
| 62 | </form> | ||
| 63 | </div> | ||
| 64 | </td> | ||
| 65 | </tr> | ||
| 66 | {% endfor %} | ||
| 67 | </tbody> | ||
| 68 | </table> | ||
| 69 | </div> | ||
| 70 | {% endblock %} | ||
diff --git a/templates/backups/list.html b/templates/backups/list.html new file mode 100644 index 0000000..d7508eb --- /dev/null +++ b/templates/backups/list.html | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | {% block content %} | ||
| 3 | <div class="page-header"> | ||
| 4 | <h1>Backups: {{ instance_name }}</h1> | ||
| 5 | <div class="actions"> | ||
| 6 | <form action="/instances/{{ instance_id }}/backups/create" method="post" style="display:inline"> | ||
| 7 | <button type="submit" class="btn-ok">Create Backup</button> | ||
| 8 | </form> | ||
| 9 | <a href="/instances/{{ instance_id }}/world/export" class="btn">Export World</a> | ||
| 10 | <a href="/instances" class="btn">Back to Instances</a> | ||
| 11 | </div> | ||
| 12 | </div> | ||
| 13 | |||
| 14 | {% if backups.is_empty() %} | ||
| 15 | <div class="card card-muted"> | ||
| 16 | <p>No backups yet. Create one to safeguard your world.</p> | ||
| 17 | </div> | ||
| 18 | {% else %} | ||
| 19 | <table class="table"> | ||
| 20 | <thead> | ||
| 21 | <tr> | ||
| 22 | <th>Filename</th> | ||
| 23 | <th>Created</th> | ||
| 24 | <th>Actions</th> | ||
| 25 | </tr> | ||
| 26 | </thead> | ||
| 27 | <tbody> | ||
| 28 | {% for backup in backups %} | ||
| 29 | <tr> | ||
| 30 | <td>{{ backup.filename }}</td> | ||
| 31 | <td>{{ backup.created_at }}</td> | ||
| 32 | <td> | ||
| 33 | <a href="/backups/{{ backup.id }}/download" class="btn-sm">Download</a> | ||
| 34 | <form action="/backups/{{ backup.id }}/restore" method="post" style="display:inline"> | ||
| 35 | <button type="submit" class="btn-sm btn-ok">Restore</button> | ||
| 36 | </form> | ||
| 37 | </td> | ||
| 38 | </tr> | ||
| 39 | {% endfor %} | ||
| 40 | </tbody> | ||
| 41 | </table> | ||
| 42 | {% endif %} | ||
| 43 | {% endblock %} | ||
diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..3a92cd0 --- /dev/null +++ b/templates/base.html | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | <meta charset="UTF-8"> | ||
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 6 | <title>Minecraft UI</title> | ||
| 7 | <script src="/static/htmx.min.js"></script> | ||
| 8 | <link rel="stylesheet" href="/static/style.css"> | ||
| 9 | </head> | ||
| 10 | <body hx-boost="true"> | ||
| 11 | <nav> | ||
| 12 | <span class="brand">Minecraft UI</span> | ||
| 13 | <div class="nav-links"> | ||
| 14 | <a href="/">Dashboard</a> | ||
| 15 | <a href="/instances">Instances</a> | ||
| 16 | <a href="/players">Players</a> | ||
| 17 | {% if is_superuser %} | ||
| 18 | <a href="/accounts">Accounts</a> | ||
| 19 | {% endif %} | ||
| 20 | <span class="nav-user">{{ username }}</span> | ||
| 21 | <form action="/logout" method="post" style="display:inline"> | ||
| 22 | <button type="submit" class="btn-link">Logout</button> | ||
| 23 | </form> | ||
| 24 | </div> | ||
| 25 | </nav> | ||
| 26 | |||
| 27 | {% if password_change_required %} | ||
| 28 | <div class="flash flash-warn"> | ||
| 29 | You must change your password before continuing. | ||
| 30 | </div> | ||
| 31 | {% endif %} | ||
| 32 | |||
| 33 | <main> | ||
| 34 | {% block content %}{% endblock %} | ||
| 35 | </main> | ||
| 36 | </body> | ||
| 37 | </html> | ||
diff --git a/templates/change_password.html b/templates/change_password.html new file mode 100644 index 0000000..6dfc11c --- /dev/null +++ b/templates/change_password.html | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | {% block content %} | ||
| 3 | <h1>Change Password</h1> | ||
| 4 | |||
| 5 | {% if !error.is_empty() %} | ||
| 6 | <div class="flash flash-err">{{ error }}</div> | ||
| 7 | {% endif %} | ||
| 8 | |||
| 9 | {% if password_change_required %} | ||
| 10 | <div class="flash flash-warn">You must change your password before continuing.</div> | ||
| 11 | {% endif %} | ||
| 12 | |||
| 13 | <form action="/change-password" method="post" class="form-col"> | ||
| 14 | <label> | ||
| 15 | Current Password | ||
| 16 | <input type="password" name="current_password" required autofocus> | ||
| 17 | </label> | ||
| 18 | <label> | ||
| 19 | New Password | ||
| 20 | <input type="password" name="new_password" required minlength="4"> | ||
| 21 | </label> | ||
| 22 | <button type="submit" class="btn-ok">Change Password</button> | ||
| 23 | </form> | ||
| 24 | {% endblock %} | ||
diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..571a43e --- /dev/null +++ b/templates/dashboard.html | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | {% block content %} | ||
| 3 | <h1>Dashboard</h1> | ||
| 4 | |||
| 5 | {% if let Some(inst) = active_instance %} | ||
| 6 | <div class="card {% if container_state == "running" %}card-ok{% else if container_state == "error" || container_state == "not_found" %}card-err{% endif %}"> | ||
| 7 | <h2>Active: {{ inst.name }}</h2> | ||
| 8 | <div class="instance-meta"> | ||
| 9 | <span>Version: {{ inst.version }}</span> | ||
| 10 | <span>Type: {{ inst.server_type }}</span> | ||
| 11 | <span>Max Players: {{ inst.max_players }}</span> | ||
| 12 | </div> | ||
| 13 | |||
| 14 | <div class="status-row"> | ||
| 15 | <div class="status-badge status-{{ container_state }}"> | ||
| 16 | {% if container_state == "running" %} | ||
| 17 | Running | ||
| 18 | {% else if container_state == "pulling" %} | ||
| 19 | Pulling image… | ||
| 20 | {% else if container_state == "starting" %} | ||
| 21 | Starting… | ||
| 22 | {% else if container_state == "stopped" %} | ||
| 23 | Stopped | ||
| 24 | {% else if container_state == "error" %} | ||
| 25 | SOMETHING BAD IS HAPPENING, CHECK DOCKER LOGS | ||
| 26 | {% else %} | ||
| 27 | {{ container_state }} | ||
| 28 | {% endif %} | ||
| 29 | </div> | ||
| 30 | </div> | ||
| 31 | |||
| 32 | <div class="actions"> | ||
| 33 | <a href="/players" class="btn">Player Management</a> | ||
| 34 | <form action="/instances/deactivate" method="post" style="display:inline"> | ||
| 35 | <button type="submit" class="btn-danger">Stop Server</button> | ||
| 36 | </form> | ||
| 37 | </div> | ||
| 38 | </div> | ||
| 39 | {% else %} | ||
| 40 | <div class="card card-muted"> | ||
| 41 | <h2>No active instance</h2> | ||
| 42 | <p>Choose a server instance to start from the <a href="/instances">Instances</a> page.</p> | ||
| 43 | </div> | ||
| 44 | {% endif %} | ||
| 45 | {% endblock %} | ||
diff --git a/templates/instances/create.html b/templates/instances/create.html new file mode 100644 index 0000000..45f70f4 --- /dev/null +++ b/templates/instances/create.html | |||
| @@ -0,0 +1,115 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | {% block content %} | ||
| 3 | <h1>Create Instance</h1> | ||
| 4 | |||
| 5 | {% if let Some(msg) = error %} | ||
| 6 | <div class="flash flash-err">{{ msg }}</div> | ||
| 7 | {% endif %} | ||
| 8 | |||
| 9 | <form action="/instances/new" method="post" class="form-col form-wide"> | ||
| 10 | <fieldset> | ||
| 11 | <legend>Basic Info</legend> | ||
| 12 | <label> | ||
| 13 | Name | ||
| 14 | <input type="text" name="name" required placeholder="My Server"> | ||
| 15 | </label> | ||
| 16 | <label> | ||
| 17 | Version | ||
| 18 | <select name="version" required> | ||
| 19 | {% for v in versions %} | ||
| 20 | <option value="{{ v }}">{{ v }}</option> | ||
| 21 | {% endfor %} | ||
| 22 | </select> | ||
| 23 | </label> | ||
| 24 | <label> | ||
| 25 | Type | ||
| 26 | <select name="server_type" required> | ||
| 27 | <option value="VANILLA">Vanilla</option> | ||
| 28 | <option value="FORGE">Forge</option> | ||
| 29 | <option value="FABRIC">Fabric</option> | ||
| 30 | </select> | ||
| 31 | </label> | ||
| 32 | <label> | ||
| 33 | Memory (MB) | ||
| 34 | <input type="number" name="memory_mb" value="4096" min="512" max="{{ max_memory_mb }}" required> | ||
| 35 | <small>512 MB – {{ max_memory_mb }} MB</small> | ||
| 36 | </label> | ||
| 37 | </fieldset> | ||
| 38 | |||
| 39 | <fieldset> | ||
| 40 | <legend>World</legend> | ||
| 41 | <label> | ||
| 42 | <input type="radio" name="world_source" value="generate" checked onchange="toggleWorldSource()"> | ||
| 43 | Generate new world | ||
| 44 | </label> | ||
| 45 | <div id="gen-options"> | ||
| 46 | <label> | ||
| 47 | Seed (optional) | ||
| 48 | <input type="text" name="seed" placeholder="Leave empty for random"> | ||
| 49 | </label> | ||
| 50 | <label> | ||
| 51 | Level Type | ||
| 52 | <select name="level_type"> | ||
| 53 | <option value="DEFAULT">Default</option> | ||
| 54 | <option value="FLAT">Flat</option> | ||
| 55 | <option value="LARGEBIOMES">Large Biomes</option> | ||
| 56 | <option value="AMPLIFIED">Amplified</option> | ||
| 57 | </select> | ||
| 58 | </label> | ||
| 59 | </div> | ||
| 60 | <label> | ||
| 61 | <input type="radio" name="world_source" value="upload" onchange="toggleWorldSource()"> | ||
| 62 | Upload world (ZIP) | ||
| 63 | </label> | ||
| 64 | <div id="upload-options" style="display:none"> | ||
| 65 | <label> | ||
| 66 | World ZIP | ||
| 67 | <input type="file" name="world_zip" accept=".zip"> | ||
| 68 | </label> | ||
| 69 | </div> | ||
| 70 | </fieldset> | ||
| 71 | |||
| 72 | <fieldset> | ||
| 73 | <legend>Server Properties</legend> | ||
| 74 | <label> | ||
| 75 | Difficulty | ||
| 76 | <select name="difficulty"> | ||
| 77 | <option value="0">Peaceful</option> | ||
| 78 | <option value="1">Easy</option> | ||
| 79 | <option value="2">Normal</option> | ||
| 80 | <option value="3" selected>Hard</option> | ||
| 81 | </select> | ||
| 82 | </label> | ||
| 83 | <label> | ||
| 84 | Gamemode | ||
| 85 | <select name="gamemode"> | ||
| 86 | <option value="0" selected>Survival</option> | ||
| 87 | <option value="1">Creative</option> | ||
| 88 | <option value="2">Adventure</option> | ||
| 89 | <option value="3">Spectator</option> | ||
| 90 | </select> | ||
| 91 | </label> | ||
| 92 | <label> | ||
| 93 | Max Players | ||
| 94 | <input type="number" name="max_players" value="20" min="1" max="100"> | ||
| 95 | </label> | ||
| 96 | <label> | ||
| 97 | <input type="checkbox" name="pvp" checked> PvP enabled | ||
| 98 | </label> | ||
| 99 | <label> | ||
| 100 | MOTD | ||
| 101 | <input type="text" name="motd" value="A Minecraft Server" maxlength="59"> | ||
| 102 | </label> | ||
| 103 | </fieldset> | ||
| 104 | |||
| 105 | <button type="submit" class="btn-ok">Create Instance</button> | ||
| 106 | </form> | ||
| 107 | |||
| 108 | <script> | ||
| 109 | function toggleWorldSource() { | ||
| 110 | var genRadio = document.querySelector('input[value="generate"]'); | ||
| 111 | document.getElementById('gen-options').style.display = genRadio.checked ? '' : 'none'; | ||
| 112 | document.getElementById('upload-options').style.display = genRadio.checked ? 'none' : ''; | ||
| 113 | } | ||
| 114 | </script> | ||
| 115 | {% endblock %} | ||
diff --git a/templates/instances/edit.html b/templates/instances/edit.html new file mode 100644 index 0000000..356c7cd --- /dev/null +++ b/templates/instances/edit.html | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | {% block content %} | ||
| 3 | <h1>Edit: {{ instance.name }}</h1> | ||
| 4 | |||
| 5 | <div class="card card-muted"> | ||
| 6 | <div class="meta"> | ||
| 7 | <span><strong>Version:</strong> {{ instance.version }}</span> | ||
| 8 | <span><strong>Type:</strong> {{ instance.server_type }}</span> | ||
| 9 | <span><strong>World:</strong> {{ instance.world_source }}</span> | ||
| 10 | {% if let Some(seed) = instance.world_seed %} | ||
| 11 | <span><strong>Seed:</strong> {{ seed }}</span> | ||
| 12 | {% endif %} | ||
| 13 | {% if instance.has_modpack %} | ||
| 14 | <span><strong>Modpack:</strong> Installed</span> | ||
| 15 | {% endif %} | ||
| 16 | </div> | ||
| 17 | </div> | ||
| 18 | |||
| 19 | <form action="/instances/{{ instance.id }}/edit" method="post" class="form-col"> | ||
| 20 | <fieldset> | ||
| 21 | <legend>Server Properties</legend> | ||
| 22 | <label> | ||
| 23 | Memory (MB) | ||
| 24 | <input type="number" name="memory_mb" value="{{ instance.memory_mb }}" min="512" max="{{ max_memory_mb }}" required> | ||
| 25 | </label> | ||
| 26 | <label> | ||
| 27 | Difficulty | ||
| 28 | <select name="difficulty"> | ||
| 29 | <option value="0" {% if instance.difficulty == 0 %}selected{% endif %}>Peaceful</option> | ||
| 30 | <option value="1" {% if instance.difficulty == 1 %}selected{% endif %}>Easy</option> | ||
| 31 | <option value="2" {% if instance.difficulty == 2 %}selected{% endif %}>Normal</option> | ||
| 32 | <option value="3" {% if instance.difficulty == 3 %}selected{% endif %}>Hard</option> | ||
| 33 | </select> | ||
| 34 | </label> | ||
| 35 | <label> | ||
| 36 | Gamemode | ||
| 37 | <select name="gamemode"> | ||
| 38 | <option value="0" {% if instance.gamemode == 0 %}selected{% endif %}>Survival</option> | ||
| 39 | <option value="1" {% if instance.gamemode == 1 %}selected{% endif %}>Creative</option> | ||
| 40 | <option value="2" {% if instance.gamemode == 2 %}selected{% endif %}>Adventure</option> | ||
| 41 | <option value="3" {% if instance.gamemode == 3 %}selected{% endif %}>Spectator</option> | ||
| 42 | </select> | ||
| 43 | </label> | ||
| 44 | <label> | ||
| 45 | Max Players | ||
| 46 | <input type="number" name="max_players" value="{{ instance.max_players }}" min="1" max="100"> | ||
| 47 | </label> | ||
| 48 | <label> | ||
| 49 | <input type="checkbox" name="pvp" {% if instance.pvp %}checked{% endif %}> PvP enabled | ||
| 50 | </label> | ||
| 51 | <label> | ||
| 52 | MOTD | ||
| 53 | <input type="text" name="motd" value="{{ instance.motd }}" maxlength="59"> | ||
| 54 | </label> | ||
| 55 | </fieldset> | ||
| 56 | |||
| 57 | <div class="actions"> | ||
| 58 | <button type="submit" class="btn-ok">Save Changes</button> | ||
| 59 | <a href="/instances" class="btn">Cancel</a> | ||
| 60 | </div> | ||
| 61 | </form> | ||
| 62 | {% endblock %} | ||
diff --git a/templates/instances/list.html b/templates/instances/list.html new file mode 100644 index 0000000..b8b2337 --- /dev/null +++ b/templates/instances/list.html | |||
| @@ -0,0 +1,50 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | {% block content %} | ||
| 3 | <div class="page-header"> | ||
| 4 | <h1>Server Instances</h1> | ||
| 5 | <a href="/instances/new" class="btn">Create Instance</a> | ||
| 6 | </div> | ||
| 7 | |||
| 8 | {% if instances.is_empty() %} | ||
| 9 | <div class="card card-muted"> | ||
| 10 | <p>No instances yet. Create one to get started.</p> | ||
| 11 | </div> | ||
| 12 | {% else %} | ||
| 13 | <div class="instance-grid"> | ||
| 14 | {% for inst in instances %} | ||
| 15 | <div class="card {% if inst.is_active %}card-active{% endif %}"> | ||
| 16 | {% if inst.is_active %} | ||
| 17 | <div class="active-badge">ACTIVE</div> | ||
| 18 | {% endif %} | ||
| 19 | <h3>{{ inst.name }}</h3> | ||
| 20 | <div class="meta"> | ||
| 21 | <span>{{ inst.version }}</span> | ||
| 22 | <span class="tag">{{ inst.server_type }}</span> | ||
| 23 | {% if inst.has_modpack %} | ||
| 24 | <span class="tag">Modded</span> | ||
| 25 | {% endif %} | ||
| 26 | </div> | ||
| 27 | <div class="actions"> | ||
| 28 | {% if !inst.is_active %} | ||
| 29 | <form action="/instances/{{ inst.id }}/activate" method="post" style="display:inline"> | ||
| 30 | <button type="submit" class="btn-ok">Activate</button> | ||
| 31 | </form> | ||
| 32 | {% else %} | ||
| 33 | <form action="/instances/deactivate" method="post" style="display:inline"> | ||
| 34 | <button type="submit" class="btn-danger">Deactivate</button> | ||
| 35 | </form> | ||
| 36 | {% endif %} | ||
| 37 | <a href="/instances/{{ inst.id }}/edit" class="btn">Edit</a> | ||
| 38 | <a href="/instances/{{ inst.id }}/backups" class="btn">Backups</a> | ||
| 39 | {% if !inst.is_active %} | ||
| 40 | <form action="/instances/{{ inst.id }}/delete" method="post" style="display:inline" | ||
| 41 | onsubmit="return confirm('Delete instance {{ inst.name }}? This cannot be undone.')"> | ||
| 42 | <button type="submit" class="btn-danger">Delete</button> | ||
| 43 | </form> | ||
| 44 | {% endif %} | ||
| 45 | </div> | ||
| 46 | </div> | ||
| 47 | {% endfor %} | ||
| 48 | </div> | ||
| 49 | {% endif %} | ||
| 50 | {% endblock %} | ||
diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..36e725e --- /dev/null +++ b/templates/login.html | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | <meta charset="UTF-8"> | ||
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 6 | <title>Login — Minecraft UI</title> | ||
| 7 | <link rel="stylesheet" href="/static/style.css"> | ||
| 8 | </head> | ||
| 9 | <body> | ||
| 10 | <main class="login-page"> | ||
| 11 | <h1>Minecraft UI</h1> | ||
| 12 | <form action="/login" method="post" class="login-form"> | ||
| 13 | <label> | ||
| 14 | Username | ||
| 15 | <input type="text" name="username" required autofocus> | ||
| 16 | </label> | ||
| 17 | <label> | ||
| 18 | Password | ||
| 19 | <input type="password" name="password" required> | ||
| 20 | </label> | ||
| 21 | <button type="submit">Login</button> | ||
| 22 | </form> | ||
| 23 | </main> | ||
| 24 | </body> | ||
| 25 | </html> | ||
diff --git a/templates/players/manage.html b/templates/players/manage.html new file mode 100644 index 0000000..5c31e4c --- /dev/null +++ b/templates/players/manage.html | |||
| @@ -0,0 +1,69 @@ | |||
| 1 | {% extends "base.html" %} | ||
| 2 | {% block content %} | ||
| 3 | <h1>Player Management</h1> | ||
| 4 | |||
| 5 | {% if let Some(name) = active_instance_name %} | ||
| 6 | <p>Managing: <strong>{{ name }}</strong></p> | ||
| 7 | {% else %} | ||
| 8 | <div class="flash flash-warn">No active instance. Start a server to manage players.</div> | ||
| 9 | {% endif %} | ||
| 10 | |||
| 11 | <div class="tab-row"> | ||
| 12 | <div class="card"> | ||
| 13 | <h3>Whitelist</h3> | ||
| 14 | <form action="/players/whitelist/add" method="post" class="form-row"> | ||
| 15 | <input type="text" name="username" placeholder="Player name" required> | ||
| 16 | <button type="submit" class="btn-ok">Add</button> | ||
| 17 | </form> | ||
| 18 | <ul class="player-list"> | ||
| 19 | {% for player in whitelist %} | ||
| 20 | <li> | ||
| 21 | {{ player }} | ||
| 22 | <form action="/players/whitelist/remove" method="post" style="display:inline"> | ||
| 23 | <input type="hidden" name="username" value="{{ player }}"> | ||
| 24 | <button type="submit" class="btn-sm btn-danger">Remove</button> | ||
| 25 | </form> | ||
| 26 | </li> | ||
| 27 | {% endfor %} | ||
| 28 | </ul> | ||
| 29 | </div> | ||
| 30 | |||
| 31 | <div class="card"> | ||
| 32 | <h3>Operators</h3> | ||
| 33 | <form action="/players/op/add" method="post" class="form-row"> | ||
| 34 | <input type="text" name="username" placeholder="Player name" required> | ||
| 35 | <button type="submit" class="btn-ok">Add</button> | ||
| 36 | </form> | ||
| 37 | <ul class="player-list"> | ||
| 38 | {% for player in ops %} | ||
| 39 | <li> | ||
| 40 | {{ player }} | ||
| 41 | <form action="/players/op/remove" method="post" style="display:inline"> | ||
| 42 | <input type="hidden" name="username" value="{{ player }}"> | ||
| 43 | <button type="submit" class="btn-sm btn-danger">Remove</button> | ||
| 44 | </form> | ||
| 45 | </li> | ||
| 46 | {% endfor %} | ||
| 47 | </ul> | ||
| 48 | </div> | ||
| 49 | |||
| 50 | <div class="card"> | ||
| 51 | <h3>Banned</h3> | ||
| 52 | <form action="/players/ban/add" method="post" class="form-row"> | ||
| 53 | <input type="text" name="username" placeholder="Player name" required> | ||
| 54 | <button type="submit" class="btn-ok">Ban</button> | ||
| 55 | </form> | ||
| 56 | <ul class="player-list"> | ||
| 57 | {% for player in bans %} | ||
| 58 | <li> | ||
| 59 | {{ player }} | ||
| 60 | <form action="/players/ban/remove" method="post" style="display:inline"> | ||
| 61 | <input type="hidden" name="username" value="{{ player }}"> | ||
| 62 | <button type="submit" class="btn-sm btn-ok">Unban</button> | ||
| 63 | </form> | ||
| 64 | </li> | ||
| 65 | {% endfor %} | ||
| 66 | </ul> | ||
| 67 | </div> | ||
| 68 | </div> | ||
| 69 | {% endblock %} | ||
