Milestone 1: workspace, CLI, SQLite schema, auth (first boot, login, sessions), SPA serving, Leptos auth UI

Co-Authored-By: Qwen3.8 27b
AuthorKonata <konata@posteo.jp>
Date
Commitfffd5424ccd4f4473e86fd375b5b77d0e4fa1bd5
25 files changed, 4545 insertions(+)
▾A.gitignore
@@ -0,0 +1,8 @@
/target
/.dev
/web/dist
/server/dist
filebrowser-ng
*.sqlite
*.sqlite-wal
*.sqlite-shm
▾ACargo.lock
@@ -0,0 +1,2879 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "any_spawner"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d"
dependencies = [
"futures",
"thiserror 2.0.20",
"wasm-bindgen-futures",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures 0.2.17",
"password-hash",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-once-cell"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
[[package]]
name = "async-trait"
version = "0.1.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "attribute-derive"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77"
dependencies = [
"attribute-derive-macro",
"derive-where",
"manyhow",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "attribute-derive-macro"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61"
dependencies = [
"collection_literals",
"interpolator",
"manyhow",
"proc-macro-utils",
"proc-macro2",
"quote",
"quote-use",
"syn 2.0.119",
]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "axum"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "base16"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "camino"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d"
[[package]]
name = "cc"
version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clap"
version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "codee"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5"
dependencies = [
"serde",
"serde_json",
"thiserror 2.0.20",
]
[[package]]
name = "collection_literals"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "config"
version = "0.15.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139"
dependencies = [
"convert_case 0.6.0",
"pathdiff",
"serde_core",
"toml",
"winnow",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "const-str"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
[[package]]
name = "const_format"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e"
dependencies = [
"const_format_proc_macros",
"konst",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "const_str_slice_concat"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case_extras"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589c70f0faf8aa9d17787557d5eae854d7755cac50f5c3d12c81d3d57661cebb"
dependencies = [
"convert_case 0.11.0",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "derive-where"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common 0.1.7",
"subtle",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.1",
"const-oid",
"crypto-common 0.2.2",
]
[[package]]
name = "displaydoc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "drain_filter_polyfill"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
[[package]]
name = "either"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]]
name = "either_of"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5060e0a4cbf26a87550792688ade88e6b8aec9208613631a7a363bda7bc2d4cd"
dependencies = [
"paste",
"pin-project-lite",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "event-listener"
version = "5.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
dependencies = [
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-executor"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-macro"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "futures-sink"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasm-bindgen",
]
[[package]]
name = "gloo-net"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils",
"http",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror 1.0.69",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "guardian"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "html-escape"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9356095b4b41197bba32173600e1582792cda618f65d12f68e2e77d273413c5"
[[package]]
name = "http"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hybrid-array"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
dependencies = [
"typenum",
]
[[package]]
name = "hydration_context"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bbbeb23ee808258cef2c5585ff0dc8e41da21a8dde943f6b290da153a042a96"
dependencies = [
"futures",
"or_poisoned",
"pin-project-lite",
"serde",
"throw_error",
]
[[package]]
name = "hyper"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "indexmap"
version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[package]]
name = "interpolator"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "konst"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb"
dependencies = [
"konst_macro_rules",
]
[[package]]
name = "konst_macro_rules"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leptos"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "705e2951f3688e0c4f66bbb7a2702282782dcee716971dbd6209c2619d272479"
dependencies = [
"any_spawner",
"cfg-if",
"either_of",
"futures",
"getrandom 0.4.3",
"hydration_context",
"leptos_config",
"leptos_dom",
"leptos_hot_reload",
"leptos_macro",
"leptos_server",
"oco_ref",
"or_poisoned",
"paste",
"reactive_graph",
"rustc-hash",
"rustc_version",
"send_wrapper",
"serde",
"serde_json",
"serde_qs",
"server_fn",
"slotmap",
"tachys",
"thiserror 2.0.20",
"throw_error",
"typed-builder",
"typed-builder-macro",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm_split_helpers",
"web-sys",
]
[[package]]
name = "leptos_config"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c06f751315bccc0d193fab302ac01d25bcfcd97474d4676440e7e3250dc3fc3"
dependencies = [
"config",
"regex",
"serde",
"thiserror 2.0.20",
"typed-builder",
]
[[package]]
name = "leptos_dom"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35742e9ed8f8aaf9e549b454c68a7ac0992536e06856365639b111f72ab07884"
dependencies = [
"js-sys",
"or_poisoned",
"reactive_graph",
"send_wrapper",
"tachys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "leptos_hot_reload"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2a0f220c8a5ef3c51199dfb9cdd702bc0eb80d52fbe70c7890adfaaae8a4b1"
dependencies = [
"anyhow",
"camino",
"indexmap",
"or_poisoned",
"proc-macro2",
"quote",
"rstml",
"serde",
"syn 2.0.119",
"walkdir",
]
[[package]]
name = "leptos_macro"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de6e8da9d4f1a7b74b447b317d590ebabb38709f588f7ee20564b773ccbcce"
dependencies = [
"attribute-derive",
"cfg-if",
"convert_case 0.11.0",
"convert_case_extras",
"html-escape",
"itertools",
"leptos_hot_reload",
"prettyplease",
"proc-macro-error2",
"proc-macro2",
"quote",
"rstml",
"rustc_version",
"server_fn_macro",
"syn 2.0.119",
"uuid",
]
[[package]]
name = "leptos_server"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da974775c5ccbb6bd64be7f53f75e8321542e28f21563a416574dbe4d5447eae"
dependencies = [
"any_spawner",
"base64",
"codee",
"futures",
"hydration_context",
"or_poisoned",
"reactive_graph",
"send_wrapper",
"serde",
"serde_json",
"server_fn",
"tachys",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libsqlite3-sys"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "litemap"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "manyhow"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
dependencies = [
"manyhow-macros",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "manyhow-macros"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
dependencies = [
"proc-macro-utils",
"proc-macro2",
"quote",
]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mio"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "next_tuple"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "oco_ref"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d"
dependencies = [
"serde",
"thiserror 2.0.20",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "or_poisoned"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "potential_utf"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
"zerovec",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.119",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "proc-macro-utils"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
dependencies = [
"proc-macro2",
"quote",
"smallvec",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"version_check",
"yansi",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "quote-use"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e"
dependencies = [
"quote",
"quote-use-macros",
]
[[package]]
name = "quote-use-macros"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35"
dependencies = [
"proc-macro-utils",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "reactive_graph"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00c5a025366836190c7030e883cc2bcd9e384ff555336e3c7954741ca411b177"
dependencies = [
"any_spawner",
"async-lock",
"futures",
"guardian",
"hydration_context",
"indexmap",
"or_poisoned",
"paste",
"pin-project-lite",
"rustc-hash",
"rustc_version",
"send_wrapper",
"serde",
"slotmap",
"thiserror 2.0.20",
"web-sys",
]
[[package]]
name = "reactive_stores"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c30fd35b7d299c591293bb69fed47a703eb2703b1cff0493e78b16ed007e5382"
dependencies = [
"guardian",
"indexmap",
"itertools",
"or_poisoned",
"paste",
"reactive_graph",
"reactive_stores_macro",
"rustc-hash",
"send_wrapper",
]
[[package]]
name = "reactive_stores_macro"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68072edd607edd30b9ebf57d984ba45d8ab8809e598d0f6046278373fb76a5a0"
dependencies = [
"convert_case 0.11.0",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "regex"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rstml"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56"
dependencies = [
"derive-where",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.119",
"syn_derive",
"thiserror 2.0.20",
]
[[package]]
name = "rusqlite"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rust-embed"
version = "8.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097"
dependencies = [
"mime_guess",
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.119",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0"
dependencies = [
"sha2 0.11.0",
"walkdir",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
dependencies = [
"futures-core",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
"serde_core",
]
[[package]]
name = "serde_qs"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352"
dependencies = [
"percent-encoding",
"serde",
"thiserror 2.0.20",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "server"
version = "0.1.0"
dependencies = [
"anyhow",
"argon2",
"axum",
"chrono",
"clap",
"mime_guess",
"rand",
"rusqlite",
"rust-embed",
"serde",
"serde_json",
"thiserror 2.0.20",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "server_fn"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be8559dd05af1b5b7e363a150616589d5a88af5187273f7f331ba0dae8922812"
dependencies = [
"base64",
"bytes",
"const-str",
"const_format",
"futures",
"gloo-net",
"http",
"js-sys",
"or_poisoned",
"pin-project-lite",
"rustc_version",
"rustversion",
"send_wrapper",
"serde",
"serde_json",
"serde_qs",
"server_fn_macro_default",
"thiserror 2.0.20",
"throw_error",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"xxhash-rust",
]
[[package]]
name = "server_fn_macro"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1295b54815397d30d986b63f93cfd515fa86d5e528e0bb589ce9d530502f9e0f"
dependencies = [
"const_format",
"convert_case 0.11.0",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.119",
"xxhash-rust",
]
[[package]]
name = "server_fn_macro_default"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00"
dependencies = [
"server_fn_macro",
"syn 2.0.119",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.3.1",
"digest 0.11.3",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "slotmap"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "socket2"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "tachys"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92ba81187437cc5df4281f2326a2e13cc81e8f96448292d1112388e2025ca66"
dependencies = [
"any_spawner",
"async-trait",
"const_str_slice_concat",
"drain_filter_polyfill",
"either_of",
"erased",
"futures",
"html-escape",
"indexmap",
"itertools",
"js-sys",
"next_tuple",
"oco_ref",
"or_poisoned",
"paste",
"reactive_graph",
"reactive_stores",
"rustc-hash",
"rustc_version",
"send_wrapper",
"slotmap",
"throw_error",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl 2.0.20",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "thread_local"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
dependencies = [
"cfg-if",
]
[[package]]
name = "throw_error"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64"
dependencies = [
"pin-project-lite",
]
[[package]]
name = "tinystr"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tokio"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "toml"
version = "1.1.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
dependencies = [
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"bitflags",
"bytes",
"http",
"http-body",
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "typed-builder"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda"
dependencies = [
"typed-builder-macro",
]
[[package]]
name = "typed-builder-macro"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
"getrandom 0.4.3",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.119",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-streams"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "wasm_split_helpers"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab578aae2fe2916edaea06843187d50f87b0965622da0ceef648edca27b385ba"
dependencies = [
"async-once-cell",
"wasm_split_macros",
]
[[package]]
name = "wasm_split_macros"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e653af7ee4a9ef0fce481a9ec6f43cb78de20d0cdb4f4f5862e1dc6e407e6c8"
dependencies = [
"base16",
"quote",
"sha2 0.10.9",
"syn 2.0.119",
]
[[package]]
name = "web"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"js-sys",
"leptos",
"serde",
"serde-wasm-bindgen",
"thiserror 2.0.20",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
dependencies = [
"memchr",
]
[[package]]
name = "writeable"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "xxhash-rust"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.8.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zerofrom"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"synstructure",
]
[[package]]
name = "zerotrie"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
▾ACargo.toml
@@ -0,0 +1,6 @@
[workspace]
resolver = "2"
members = ["server", "web"]
[profile.release]
strip = true
▾AREADME.md
@@ -0,0 +1,59 @@
# filebrowser-ng
A simple, noob-friendly web file browser. Single binary, SQLite-backed.
## Features (status)
- [x] First-boot admin setup (create the admin account in the browser)
- [x] Login/logout with cookie sessions (argon2id password hashing)
- [x] Per-user folder access ("virtual roots"), read-write or read-only
- [ ] File browsing (grid/list, breadcrumbs, context menu)
- [ ] Upload (files & folders, drag & drop, overwrite warning)
- [ ] Download (files as-is; folders as zip / tar / tar.gz / tar.zst, streamed)
- [ ] Move / copy / rename / delete / new folder
- [ ] File info, previews (image/PDF/video/audio/text), text editor (CodeMirror)
- [ ] Shares (token links, optional expiry, optional writable, admin toggle)
- [ ] Admin UI (user management, settings)
## Usage
```
filebrowser-ng --root /path/to/files --db /path/to/filebrowser.db
[--port 8080] [--bind 127.0.0.1] [--https]
```
- `--root` — the folder the server has access to (required)
- `--db` — path to the SQLite database (required)
- `--https` — assume a TLS-terminating reverse proxy in front (Secure cookies)
First run: open the URL and create the admin account. The admin gets the
whole root as their folder.
## Development
```sh
just dev-server # backend on :8081 (needs the dev data dir; created automatically)
just dev-web # Trunk dev server on :8080, proxies /api to :8081
```
Open http://localhost:8080 .
Production single binary:
```sh
just build # trunk build + embed into server/dist + cargo build --release
just run # build, then run against .dev/root and .dev/db.sqlite
```
Reset dev users/settings: `just reset-db`
## Architecture
- `server/` — Axum (Rust). SQLite via `rusqlite` (bundled). All user paths are
stored **relative to `--root`**; every filesystem operation resolves
`<root>/<user-root>/<requested-path>`, canonicalizes it, and verifies it is
still inside the user's root (blocks `..` and symlink escapes). Writes
additionally require the root to be `rw`.
- `web/` — Leptos 0.8 CSR, built with Trunk. In production the frontend is
embedded into the binary (`--features embedded`, folder `server/dist`); in
dev it is served from `web/dist` on disk.
▾Ajustfile
@@ -0,0 +1,36 @@
# filebrowser-ng developer tasks
dev-root := '.dev/root'
dev-db := '.dev/db.sqlite'
default: dev
# Create the dev root folder.
dev-data:
mkdir -p {{dev-root}}
# Backend (dev): serves the frontend from Trunk's output directory.
# Frontend dev server: just dev-web (port 8080)
# Backend dev server: just dev-server (port 8081, /api proxied by Trunk)
dev: dev-data dev-server
dev-server:
cargo run -p server -- --root {{dev-root}} --db {{dev-db}} --port 8081
dev-web:
cd web && trunk serve --proxy-rewrite /api --proxy-backend http://127.0.0.1:8081
# Production single binary (frontend embedded).
build:
cd web && trunk build --release
rm -rf server/dist
cp -r web/dist server/dist
cargo build --release -p server --features embedded
# Run the production binary against the dev folders.
run: build
./target/release/filebrowser-ng --root {{dev-root}} --db {{dev-db}}
# Remove the dev database (factory reset of users/shares/settings).
reset-db:
rm -f {{dev-db}} {{dev-db}}-wal {{dev-db}}-shm
▾Aserver/Cargo.toml
@@ -0,0 +1,35 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "filebrowser-ng"
path = "src/main.rs"
[dependencies]
anyhow = "1"
argon2 = "0.5"
axum = "0.8"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
mime_guess = "2"
rand = "0.8"
rusqlite = { version = "0.37", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "fs"] }
tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dependencies.rust-embed]
version = "8"
optional = true
[features]
# Embed the frontend (built into server/dist by `just build`).
# Without this feature the server reads the frontend from disk at runtime
# (Trunk's output directory, or $FBNG_DIST), which is the dev flow.
embedded = ["dep:rust-embed"]
▾Aserver/src/api/auth.rs
@@ -0,0 +1,160 @@
use std::path::Path;
use std::sync::Arc;
use axum::extract::State;
use axum::http::{header, HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Deserialize;
use crate::auth::{self, clear_session_cookie, parse_session_cookie, session_cookie};
use crate::error::{ApiError, AppState};
#[derive(Deserialize)]
pub struct CredentialsBody {
pub name: String,
pub password: String,
}
/// GET /api/auth/me
///
/// - No users at all → `200 {"first_boot": true}`
/// - No/invalid session → `401`
/// - Valid session → user info + visible roots
pub async fn me(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<serde_json::Value>, ApiError> {
if state.db.user_count().await == 0 {
return Ok(Json(serde_json::json!({
"first_boot": true,
"user": null,
"roots": []
})));
}
let Some(token) = parse_session_cookie(&headers) else {
return Err(ApiError::new(
StatusCode::UNAUTHORIZED,
"not signed in",
));
};
let Some(user) = state.db.session_user(&token).await else {
return Err(ApiError::new(
StatusCode::UNAUTHORIZED,
"session expired, please sign in again",
));
};
let roots = state
.db
.user_roots(user.id)
.await
.into_iter()
.map(|r| {
serde_json::json!({
"id": r.id,
"name": display_name(&state.root, &r.path),
"path": r.path,
"mode": r.mode,
})
})
.collect::<Vec<_>>();
Ok(Json(serde_json::json!({
"first_boot": false,
"user": {
"id": user.id,
"name": user.name,
"is_admin": user.is_admin,
},
"roots": roots
})))
}
/// Display name for a user root: the folder name, or the root folder's
/// own name when the user root is the whole root (".").
fn display_name(server_root: &Path, rel: &str) -> String {
let p = Path::new(rel);
let name = if rel == "." {
server_root.file_name()
} else {
p.file_name().filter(|_| !p.as_os_str().is_empty())
};
name.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| rel.to_string())
}
/// POST /api/auth/setup — create the first admin account.
/// Only available while no users exist.
pub async fn setup(
State(state): State<Arc<AppState>>,
Json(body): Json<CredentialsBody>,
) -> Result<Response, ApiError> {
let name = body.name.trim();
if name.is_empty() || name.len() > 64 {
return Err(ApiError::new(
StatusCode::BAD_REQUEST,
"name must be 1–64 characters",
));
}
if body.password.len() < 8 {
return Err(ApiError::new(
StatusCode::BAD_REQUEST,
"password must be at least 8 characters",
));
}
if state.db.user_count().await > 0 {
return Err(ApiError::new(
StatusCode::CONFLICT,
"server is already set up",
));
}
let pass_hash = auth::hash_password(&body.password)
.map_err(|e| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, format!("hashing failed: {e}")))?;
let user = state.db.create_admin(name, &pass_hash).await?;
let token = auth::random_token();
state.db.create_session(user.id, &token).await?;
let mut res = Json(serde_json::json!({ "ok": true })).into_response();
res.headers_mut()
.insert(header::SET_COOKIE, session_cookie(&token, state.https).parse().unwrap());
Ok(res)
}
/// POST /api/auth/login
pub async fn login(
State(state): State<Arc<AppState>>,
Json(body): Json<CredentialsBody>,
) -> Result<Response, ApiError> {
let Some(user) = state.db.verify_password(&body.name, &body.password).await else {
return Err(ApiError::new(
StatusCode::UNAUTHORIZED,
"invalid name or password",
));
};
let token = auth::random_token();
state.db.create_session(user.id, &token).await?;
let mut res = Json(serde_json::json!({ "ok": true })).into_response();
res.headers_mut()
.insert(header::SET_COOKIE, session_cookie(&token, state.https).parse().unwrap());
Ok(res)
}
/// POST /api/auth/logout
pub async fn logout(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Response {
if let Some(token) = parse_session_cookie(&headers) {
let _ = state.db.delete_session(&token).await;
}
let mut res = Json(serde_json::json!({ "ok": true })).into_response();
res.headers_mut()
.insert(header::SET_COOKIE, clear_session_cookie(state.https).parse().unwrap());
res
}
▾Aserver/src/api/mod.rs
@@ -0,0 +1,19 @@
use std::sync::Arc;
use axum::routing::{get, post};
use axum::Router;
use crate::error::AppState;
mod auth;
mod spa;
pub fn router(state: Arc<AppState>) -> Router {
Router::new()
.route("/api/auth/login", post(auth::login))
.route("/api/auth/logout", post(auth::logout))
.route("/api/auth/me", get(auth::me))
.route("/api/auth/setup", post(auth::setup))
.fallback(spa::fallback)
.with_state(state)
}
▾Aserver/src/api/spa.rs
@@ -0,0 +1,52 @@
use axum::http::Uri;
use axum::http::{header, Method, StatusCode};
use axum::response::{IntoResponse, Response};
const DEV_HINT: &str = r#"<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>filebrowser-ng</title></head>
<body style="font-family:system-ui;display:grid;place-items:center;min-height:100vh;margin:0;background:#f6f7f9">
<div style="max-width:520px;padding:24px;border:1px solid #e5e7eb;border-radius:12px;background:#fff">
<h1>filebrowser-ng</h1>
<p>The frontend has not been built yet.</p>
<p>In development, run <code>just dev-web</code> in a second terminal and open
<code>http://localhost:8080</code> (Trunk serves the app and proxies <code>/api</code> to this backend).</p>
<p>If you hit this page directly from the backend, make sure Trunk's output
directory is visible — the server looks in <code>target/web</code> or <code>$FBNG_DIST</code>.</p>
<p>For the production single binary: <code>just build</code>.</p>
</div></body></html>"#;
/// Serve the frontend: static assets, with a fallback to index.html so that
/// client-side routes (e.g. `/s/<token>`) work.
pub(super) async fn fallback(method: Method, uri: Uri) -> Response {
let path = uri.path();
if path.starts_with("/api/") {
return (StatusCode::NOT_FOUND, "unknown endpoint").into_response();
}
if method != Method::GET && method != Method::HEAD {
return StatusCode::METHOD_NOT_ALLOWED.into_response();
}
let rel = path.trim_start_matches('/');
let asset = if rel.is_empty() {
crate::assets::get_asset("index.html")
} else {
crate::assets::get_asset(rel)
};
match asset {
Some((bytes, mime, cache)) => {
(
[(header::CONTENT_TYPE, mime), (header::CACHE_CONTROL, cache)],
bytes,
)
.into_response()
}
None => match crate::assets::get_asset("index.html") {
Some((bytes, mime, _)) => {
([(header::CONTENT_TYPE, mime), (header::CACHE_CONTROL, "no-cache".to_string())], bytes)
.into_response()
}
None => DEV_HINT.into_response(),
},
}
}
▾Aserver/src/assets.rs
@@ -0,0 +1,51 @@
//! Static frontend assets: embedded at compile time (`--features embedded`)
//! or read from disk in the dev flow (Trunk's output dir or $FBNG_DIST).
use std::path::PathBuf;
#[cfg(feature = "embedded")]
mod embedded {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "dist/"]
pub struct Assets;
}
/// Look up an asset by (slash-separated) path.
/// Returns (bytes, content-type, cache-control).
pub(crate) fn get_asset(path: &str) -> Option<(Vec<u8>, String, String)> {
let (bytes, from_disk) = read(path)?;
let mime = mime_guess::from_path(path).first_or_octet_stream().to_string();
let cache = if from_disk || path == "index.html" {
"no-cache".to_string()
} else {
// Trunk hashes asset file names, so they are safe to cache forever.
"public, max-age=31536000, immutable".to_string()
};
Some((bytes, mime, cache))
}
#[cfg(feature = "embedded")]
fn read(path: &str) -> Option<(Vec<u8>, bool)> {
embedded::Assets::get(path).map(|c| (c.to_vec(), false))
}
#[cfg(not(feature = "embedded"))]
fn dev_dist_dir() -> PathBuf {
std::env::var_os("FBNG_DIST")
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../web/dist")
})
}
#[cfg(not(feature = "embedded"))]
fn read(path: &str) -> Option<(Vec<u8>, bool)> {
let p = dev_dist_dir().join(path);
if p.is_file() {
std::fs::read(&p).ok().map(|b| (b, true))
} else {
None
}
}
▾Aserver/src/auth.rs
@@ -0,0 +1,65 @@
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::Argon2;
pub const COOKIE_NAME: &str = "fbng_session";
/// 30 days.
pub const SESSION_MAX_AGE: u64 = 60 * 60 * 24 * 30;
pub fn hash_password(password: &str) -> anyhow::Result<String> {
let salt = SaltString::generate(&mut rand::thread_rng());
let hash = Argon2::default()
.hash_password(password.as_bytes(), &salt)
.map_err(|e| anyhow::anyhow!("password hashing failed: {e}"))?;
Ok(hash.to_string())
}
pub fn verify_password(password: &str, hash: &str) -> bool {
let Ok(parsed) = PasswordHash::new(hash) else {
return false;
};
Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok()
}
/// 32 random bytes, hex-encoded (64 chars).
pub fn random_token() -> String {
use rand::RngCore;
let mut b = [0u8; 32];
rand::thread_rng().fill_bytes(&mut b);
let mut s = String::with_capacity(64);
for x in b {
s.push_str(&format!("{x:02x}"));
}
s
}
pub fn session_cookie(token: &str, https: bool) -> String {
let mut c = format!(
"{COOKIE_NAME}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_MAX_AGE}"
);
if https {
c.push_str("; Secure");
}
c
}
pub fn clear_session_cookie(https: bool) -> String {
let mut c = format!("{COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
if https {
c.push_str("; Secure");
}
c
}
/// Extract the session token from the Cookie header, if present.
pub fn parse_session_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
let header = headers.get(axum::http::header::COOKIE)?.to_str().ok()?;
for part in header.split(';') {
let part = part.trim();
if let Some((k, v)) = part.split_once('=') {
if k == COOKIE_NAME && !v.is_empty() {
return Some(v.to_string());
}
}
}
None
}
▾Aserver/src/cli.rs
@@ -0,0 +1,33 @@
use std::path::PathBuf;
use clap::Parser;
#[derive(Parser, Debug)]
#[command(
name = "filebrowser-ng",
about = "A simple, noob-friendly web file browser",
version
)]
pub struct Cli {
/// Root folder the server has access to (required).
/// All user folders and shares are relative to this.
#[arg(long)]
pub root: PathBuf,
/// Path to the SQLite database file (required).
#[arg(long)]
pub db: PathBuf,
/// Port to listen on.
#[arg(long, default_value_t = 8080)]
pub port: u16,
/// Address to bind. Use 0.0.0.0 to expose beyond localhost.
#[arg(long, default_value = "127.0.0.1")]
pub bind: String,
/// Assume the server runs behind a TLS-terminating reverse proxy
/// (sets the `Secure` attribute on the session cookie).
#[arg(long)]
pub https: bool,
}
▾Aserver/src/db.rs
@@ -0,0 +1,256 @@
use std::path::Path;
use std::sync::Arc;
use rusqlite::{params, Connection, OptionalExtension};
const SCHEMA_VERSION: i64 = 1;
#[derive(Debug, Clone)]
pub struct User {
pub id: i64,
pub name: String,
pub is_admin: bool,
}
#[derive(Debug, Clone)]
pub struct RootRow {
pub id: i64,
/// Path relative to the server root; "." means the whole root.
pub path: String,
/// "rw" or "ro"
pub mode: String,
}
#[derive(Clone)]
pub struct Db(Arc<tokio::sync::Mutex<Connection>>);
impl std::fmt::Debug for Db {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Db").finish()
}
}
impl Db {
pub async fn open(path: &Path) -> anyhow::Result<Self> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
let conn = Connection::open(path)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.pragma_update(None, "busy_timeout", "5000")?;
Self::migrate(&conn)?;
Ok(Self(Arc::new(tokio::sync::Mutex::new(conn))))
}
fn migrate(conn: &Connection) -> rusqlite::Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)",
[],
)?;
let version: i64 = conn
.query_row("SELECT value FROM meta WHERE key = 'schema_version'", [], |r| {
r.get::<_, String>(0)
})
.optional()?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
if version < 1 {
conn.execute_batch(SCHEMA_V1)?;
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?1)",
[SCHEMA_VERSION.to_string()],
)?;
}
Ok(())
}
// ---------- users ----------
pub async fn user_count(&self) -> i64 {
let c = self.0.lock().await;
c.query_row("SELECT COUNT(*) FROM users", [], |r| r.get(0))
.unwrap_or(0)
}
/// Create the first admin account with the whole root visible (read-write).
/// Only valid while no users exist (enforced by the caller).
pub async fn create_admin(&self, name: &str, pass_hash: &str) -> Result<User, rusqlite::Error> {
let mut c = self.0.lock().await;
let tx = c.transaction()?;
tx.execute(
"INSERT INTO users (name, pass_hash, is_admin, created_at)
VALUES (?1, ?2, 1, ?3)",
params![name, pass_hash, now()],
)?;
let user_id = tx.last_insert_rowid();
tx.execute(
"INSERT INTO user_roots (user_id, path, mode) VALUES (?1, '.', 'rw')",
params![user_id],
)?;
tx.commit()?;
Ok(User {
id: user_id,
name: name.to_string(),
is_admin: true,
})
}
pub async fn verify_password(&self, name: &str, password: &str) -> Option<User> {
let c = self.0.lock().await;
let row: Option<(i64, String, bool, String)> = c
.query_row(
"SELECT id, name, is_admin != 0, pass_hash FROM users WHERE name = ?1",
[name],
|r| {
Ok((
r.get(0)?,
r.get(1)?,
r.get(2)?,
r.get(3)?,
))
},
)
.optional()
.ok()
.flatten();
let (id, name, is_admin, hash) = row?;
crate::auth::verify_password(password, &hash).then_some(User {
id,
name,
is_admin,
})
}
// ---------- sessions ----------
pub async fn create_session(&self, user_id: i64, token: &str) -> Result<(), rusqlite::Error> {
let c = self.0.lock().await;
c.execute(
"INSERT INTO sessions (token, user_id, created_at, last_seen_at)
VALUES (?1, ?2, ?3, ?4)",
params![token, user_id, now(), now()],
)?;
Ok(())
}
pub async fn delete_session(&self, token: &str) -> Result<(), rusqlite::Error> {
let c = self.0.lock().await;
c.execute("DELETE FROM sessions WHERE token = ?1", [token])?;
Ok(())
}
pub async fn session_user(&self, token: &str) -> Option<User> {
let c = self.0.lock().await;
c.query_row(
"SELECT u.id, u.name, u.is_admin != 0
FROM sessions s JOIN users u ON u.id = s.user_id
WHERE s.token = ?1",
[token],
|r| {
Ok(User {
id: r.get(0)?,
name: r.get(1)?,
is_admin: r.get(2)?,
})
},
)
.ok()
}
// ---------- roots ----------
pub async fn user_roots(&self, user_id: i64) -> Vec<RootRow> {
let c = self.0.lock().await;
let mut out = Vec::new();
let Ok(mut stmt) = c
.prepare("SELECT id, path, mode FROM user_roots WHERE user_id = ?1 ORDER BY id")
else {
return out;
};
if let Ok(rows) =
stmt.query_map([user_id], |r| {
Ok(RootRow {
id: r.get(0)?,
path: r.get(1)?,
mode: r.get(2)?,
})
})
{
out.extend(rows.flatten());
}
out
}
// ---------- settings ----------
// Used from later milestones (shares/admin settings).
#[allow(dead_code)]
pub async fn get_setting(&self, key: &str) -> Option<String> {
let c = self.0.lock().await;
c.query_row("SELECT value FROM settings WHERE key = ?1", [key], |r| {
r.get(0)
})
.ok()
}
#[allow(dead_code)]
pub async fn set_setting(&self, key: &str, value: &str) -> Result<(), rusqlite::Error> {
let c = self.0.lock().await;
c.execute(
"INSERT INTO settings (key, value) VALUES (?1, ?2)
ON CONFLICT(key) DO UPDATE SET value = ?2",
params![key, value],
)?;
Ok(())
}
}
fn now() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
const SCHEMA_V1: &str = r#"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
pass_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_roots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
path TEXT NOT NULL,
mode TEXT NOT NULL CHECK (mode IN ('rw','ro')),
UNIQUE (user_id, path)
);
CREATE TABLE IF NOT EXISTS shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
creator_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
target TEXT NOT NULL,
is_file INTEGER NOT NULL,
mode TEXT NOT NULL CHECK (mode IN ('rw','ro')),
created_at TEXT NOT NULL,
expires_at TEXT
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT OR IGNORE INTO settings (key, value) VALUES ('allow_writable_shares', '0');
"#;
▾Aserver/src/error.rs
@@ -0,0 +1,42 @@
use std::path::PathBuf;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use crate::db::Db;
#[derive(Clone)]
pub struct AppState {
pub db: Db,
/// Canonicalized server root folder.
pub root: PathBuf,
/// Whether we sit behind a TLS-terminating reverse proxy.
pub https: bool,
}
#[derive(Debug, thiserror::Error)]
#[error("{1}")]
pub struct ApiError(pub StatusCode, pub String);
impl ApiError {
pub fn new(status: StatusCode, msg: impl Into<String>) -> Self {
Self(status, msg.into())
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(
self.0,
axum::Json(serde_json::json!({ "error": self.1 })),
)
.into_response()
}
}
impl From<rusqlite::Error> for ApiError {
fn from(e: rusqlite::Error) -> Self {
tracing::error!(error = %e, "database error");
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, "internal error")
}
}
▾Aserver/src/main.rs
@@ -0,0 +1,58 @@
mod api;
mod assets;
mod auth;
mod cli;
mod db;
mod error;
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use anyhow::{bail, Context};
use clap::Parser;
use tower_http::trace::TraceLayer;
use crate::cli::Cli;
use crate::db::Db;
use crate::error::AppState;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,tower_http=warn".into()),
)
.init();
let cli = Cli::parse();
let root = cli
.root
.canonicalize()
.with_context(|| format!("cannot resolve root folder: {}", cli.root.display()))?;
if !root.is_dir() {
bail!("root folder is not a directory: {}", root.display());
}
let db = Db::open(&cli.db).await?;
let state = Arc::new(AppState {
db,
root: root.clone(),
https: cli.https,
});
let app = api::router(state).layer(TraceLayer::new_for_http());
let ip: IpAddr = cli.bind.parse().context("invalid --bind address")?;
let addr = SocketAddr::new(ip, cli.port);
let listener = tokio::net::TcpListener::bind(addr)
.await
.with_context(|| format!("cannot bind to {addr}"))?;
tracing::info!(root = %root.display(), "filebrowser-ng starting");
tracing::info!(addr = %addr, "listening (pass --bind 0.0.0.0 to expose beyond localhost)");
axum::serve(listener, app).await?;
Ok(())
}
▾Aweb/Cargo.toml
@@ -0,0 +1,27 @@
[package]
name = "web"
version = "0.1.0"
edition = "2021"
[dependencies]
console_error_panic_hook = "0.1"
js-sys = "0.3"
leptos = { version = "0.8", features = ["csr"] }
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
thiserror = "2"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = [
"Document",
"Headers",
"HtmlInputElement",
"Location",
"Navigator",
"Request",
"RequestInit",
"RequestMode",
"Response",
"SubmitEvent",
"Window",
] }
▾Aweb/app.css
@@ -0,0 +1,230 @@
:root {
--bg: #f6f7f9;
--panel: #ffffff;
--text: #1a1d21;
--muted: #6b7280;
--accent: #3b82f6;
--accent-hover: #2563eb;
--danger: #ef4444;
--border: #e5e7eb;
--radius: 12px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f1115;
--panel: #171a20;
--text: #e5e7eb;
--muted: #9ca3af;
--accent: #60a5fa;
--accent-hover: #3b82f6;
--danger: #f87171;
--border: #2a2f3a;
}
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 15px;
}
.muted {
color: var(--muted);
}
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
}
.center-screen {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.auth-card {
width: 100%;
max-width: 380px;
}
.auth-card h1 {
margin: 0 0 8px;
font-size: 22px;
}
.auth-card p {
margin: 0 0 20px;
font-size: 13px;
}
.field {
display: block;
margin-bottom: 14px;
}
.field span {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 4px;
}
input {
width: 100%;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
font-size: 14px;
}
input:focus {
outline: 2px solid var(--accent);
outline-offset: -1px;
}
button {
font: inherit;
cursor: pointer;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--panel);
color: var(--text);
padding: 9px 14px;
}
button:hover {
border-color: var(--muted);
}
button:disabled {
opacity: 0.6;
cursor: default;
}
.btn-primary {
width: 100%;
background: var(--accent);
border-color: var(--accent);
color: #fff;
font-weight: 600;
}
.btn-primary:hover {
background: var(--accent-hover);
border-color: var(--accent-hover);
}
.error {
color: var(--danger);
font-size: 13px;
min-height: 18px;
margin: 0 0 10px;
}
/* layout (signed-in) */
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
background: var(--panel);
border-bottom: 1px solid var(--border);
}
.brand {
font-weight: 700;
}
.topbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.user-badge {
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: var(--bg);
border: 1px solid var(--border);
color: var(--muted);
}
.content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 16px;
}
.welcome-card {
width: 100%;
max-width: 640px;
}
.welcome-card h1 {
margin: 0 0 8px;
font-size: 22px;
}
.welcome-card p {
margin: 0 0 16px;
font-size: 14px;
}
.section-title {
font-size: 15px;
margin: 0 0 4px;
}
.roots {
list-style: none;
margin: 0;
padding: 0;
}
.roots li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-top: 1px solid var(--border);
}
.roots .root-name {
font-weight: 600;
}
.roots .muted {
font-size: 13px;
flex: 1;
}
▾Aweb/index.html
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>filebrowser-ng</title>
<link data-trunk rel="css" href="app.css" />
</head>
<body>
<link data-trunk rel="rust" data-wasm-opt="z" />
<noscript>filebrowser-ng requires JavaScript.</noscript>
</body>
</html>
▾Aweb/src/api.rs
@@ -0,0 +1,167 @@
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("{1}")]
Http(u16, String),
#[error("network error: {0}")]
Net(String),
}
#[derive(Deserialize, Clone)]
pub struct Me {
pub first_boot: bool,
#[serde(default)]
pub user: Option<UserInfo>,
#[serde(default)]
pub roots: Vec<RootInfo>,
}
#[derive(Deserialize, Clone)]
pub struct UserInfo {
#[allow(dead_code)] // used from milestone 2 onwards
pub id: i64,
pub name: String,
pub is_admin: bool,
}
#[derive(Deserialize, Clone)]
pub struct RootInfo {
#[allow(dead_code)] // used from milestone 2 onwards
pub id: i64,
pub name: String,
pub path: String,
pub mode: String,
}
#[derive(Deserialize)]
pub struct OkResp;
#[derive(Serialize)]
struct CredentialsBody {
name: String,
password: String,
}
pub fn me() -> impl std::future::Future<Output = Result<Me, ApiError>> {
request("GET", "/api/auth/me", None::<()>)
}
pub fn login(
name: String,
password: String,
) -> impl std::future::Future<Output = Result<OkResp, ApiError>> {
request(
"POST",
"/api/auth/login",
Some(CredentialsBody { name, password }),
)
}
pub fn setup(
name: String,
password: String,
) -> impl std::future::Future<Output = Result<OkResp, ApiError>> {
request(
"POST",
"/api/auth/setup",
Some(CredentialsBody { name, password }),
)
}
pub fn logout() -> impl std::future::Future<Output = Result<OkResp, ApiError>> {
request("POST", "/api/auth/logout", Some(()))
}
async fn request<T: DeserializeOwned>(
method: &str,
url: &str,
body: Option<impl Serialize>,
) -> Result<T, ApiError> {
let window =
web_sys::window().ok_or_else(|| ApiError::Net("no window available".to_string()))?;
let opts = web_sys::RequestInit::new();
opts.set_method(method);
opts.set_mode(web_sys::RequestMode::SameOrigin);
if let Some(body) = body {
let json = serde_json_to_string(&body)
.map_err(|e| ApiError::Net(e.to_string()))?;
opts.set_body_opt_str(Some(&json));
let headers = web_sys::Headers::new()
.expect("Headers constructor failed");
let _ = headers.set("Content-Type", "application/json");
opts.set_headers_headers(&headers);
}
let req =
web_sys::Request::new_with_str_and_init(url, &opts)
.map_err(|e| ApiError::Net(format!("{e:?}")))?;
let promise = window.fetch_with_request(&req);
let resp_val = JsFuture::from(promise)
.await
.map_err(|e| ApiError::Net(format!("{e:?}")))?;
let resp: web_sys::Response = resp_val
.dyn_into()
.map_err(|_| ApiError::Net("fetch did not return a Response".to_string()))?;
let status = resp.status();
if !(200..300).contains(&status) {
return Err(ApiError::Http(status, error_message(&resp).await));
}
let json_promise = resp
.json()
.map_err(|e| ApiError::Net(format!("{e:?}")))?;
let js: JsValue = JsFuture::from(json_promise)
.await
.map_err(|e| ApiError::Net(format!("response is not JSON: {e:?}")))?;
serde_wasm_bindgen::from_value(js).map_err(|e| ApiError::Net(e.to_string()))
}
/// Best-effort extraction of the server's `{"error": "..."}` message.
async fn error_message(resp: &web_sys::Response) -> String {
let Ok(promise) = resp.text() else {
return "request failed".to_string();
};
let Ok(js) = JsFuture::from(promise).await else {
return "request failed".to_string();
};
let Some(text) = js.as_string() else {
return "request failed".to_string();
};
const MARKER: &str = "\"error\":\"";
if let Some(start) = text.find(MARKER) {
let rest = &text[start + MARKER.len()..];
if let Some(end) = rest.find('"') {
return rest[..end].replace("\\\"", "\"");
}
}
"request failed".to_string()
}
/// Read a form input's value by element id.
pub fn input_value(id: &str) -> String {
web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id(id).and_then(|el| el.dyn_into::<web_sys::HtmlInputElement>().ok()))
.map(|i| i.value())
.unwrap_or_default()
}
fn serde_json_to_string(v: &impl Serialize) -> Result<String, serde_wasm_bindgen::Error> {
// Reuse the wasm-bindgen JSON serializer; the body must be a plain string
// so we convert via JSON text.
let value = serde_wasm_bindgen::to_value(v)?;
let s = js_sys::JSON::stringify(&value).map_err(|e| {
serde_wasm_bindgen::Error::new(format!("JSON.stringify failed: {e:?}"))
})?;
s.as_string().ok_or_else(|| {
serde_wasm_bindgen::Error::new("stringify returned a non-string")
})
}
▾Aweb/src/app.rs
@@ -0,0 +1,63 @@
use leptos::prelude::*;
use wasm_bindgen_futures::spawn_local;
use crate::api::{self, Me};
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum AuthPhase {
Loading,
Setup,
Login,
Authed,
}
#[component]
pub fn App() -> impl IntoView {
let (phase, set_phase) = signal(AuthPhase::Loading);
let (me, set_me) = signal(Option::<Me>::None);
// Initial /me probe: first boot, signed in, or needs login.
spawn_local(async move {
match api::me().await {
Ok(m) => {
let first_boot = m.first_boot;
set_me.set(Some(m));
set_phase.set(if first_boot {
AuthPhase::Setup
} else {
AuthPhase::Authed
});
}
Err(_) => set_phase.set(AuthPhase::Login),
}
});
view! {
{move || {
match phase.get() {
AuthPhase::Loading => view! {
<div class="center-screen">
<p class="muted">"Loading…"</p>
</div>
}
.into_view()
.into_any(),
AuthPhase::Setup => view! {
<crate::views::setup::SetupView set_me=set_me set_phase=set_phase/>
}
.into_view()
.into_any(),
AuthPhase::Login => view! {
<crate::views::login::LoginView set_me=set_me set_phase=set_phase/>
}
.into_view()
.into_any(),
AuthPhase::Authed => view! {
<crate::views::shell::ShellView me=me set_me=set_me set_phase=set_phase/>
}
.into_view()
.into_any(),
}
}}
}
}
▾Aweb/src/main.rs
@@ -0,0 +1,8 @@
mod api;
mod app;
mod views;
fn main() {
console_error_panic_hook::set_once();
leptos::mount::mount_to_body(app::App);
}
▾Aweb/src/views/login.rs
@@ -0,0 +1,77 @@
use leptos::prelude::*;
use wasm_bindgen_futures::spawn_local;
use crate::api::{self, input_value, Me};
use crate::app::AuthPhase;
#[component]
pub fn LoginView(
set_me: WriteSignal<Option<Me>>,
set_phase: WriteSignal<AuthPhase>,
) -> impl IntoView {
let (error, set_error) = signal(String::new());
let (busy, set_busy) = signal(false);
let on_submit = move |ev: web_sys::SubmitEvent| {
ev.prevent_default();
let name = input_value("login-name");
let pass = input_value("login-pass");
if name.trim().is_empty() || pass.is_empty() {
set_error.set("Please enter your name and password.".into());
return;
}
set_error.set(String::new());
set_busy.set(true);
let name = name.trim().to_string();
spawn_local(async move {
match api::login(name, pass).await {
Ok(_) => match api::me().await {
Ok(m) => {
set_me.set(Some(m));
set_phase.set(AuthPhase::Authed);
}
Err(e) => {
set_error.set(e.to_string());
set_busy.set(false);
}
},
Err(e) => {
set_error.set(e.to_string());
set_busy.set(false);
}
}
});
};
view! {
<div class="center-screen">
<div class="card auth-card">
<h1>"filebrowser-ng"</h1>
<p class="muted">"Sign in to continue."</p>
<form on:submit=on_submit>
<label class="field">
<span>"Name"</span>
<input
id="login-name"
type="text"
autofocus=true
autocomplete="username"
/>
</label>
<label class="field">
<span>"Password"</span>
<input
id="login-pass"
type="password"
autocomplete="current-password"
/>
</label>
<p class="error">{move || error.get()}</p>
<button class="btn-primary" type="submit" disabled=move || busy.get()>
"Sign in"
</button>
</form>
</div>
</div>
}
}
▾Aweb/src/views/mod.rs
@@ -0,0 +1,3 @@
pub mod login;
pub mod setup;
pub mod shell;
▾Aweb/src/views/setup.rs
@@ -0,0 +1,100 @@
use leptos::prelude::*;
use wasm_bindgen_futures::spawn_local;
use crate::api::{self, input_value, Me};
use crate::app::AuthPhase;
#[component]
pub fn SetupView(
set_me: WriteSignal<Option<Me>>,
set_phase: WriteSignal<AuthPhase>,
) -> impl IntoView {
let (error, set_error) = signal(String::new());
let (busy, set_busy) = signal(false);
let on_submit = move |ev: web_sys::SubmitEvent| {
ev.prevent_default();
let name = input_value("setup-name");
let pass = input_value("setup-pass");
let confirm = input_value("setup-confirm");
let name = name.trim();
if name.is_empty() || name.len() > 64 {
set_error.set("Please choose a name (1–64 characters).".into());
return;
}
if pass.len() < 8 {
set_error.set("Password must be at least 8 characters.".into());
return;
}
if pass != confirm {
set_error.set("Passwords don't match.".into());
return;
}
set_error.set(String::new());
set_busy.set(true);
let (name, pass) = (name.to_string(), pass);
spawn_local(async move {
match api::setup(name, pass).await {
Ok(_) => match api::me().await {
Ok(m) => {
set_me.set(Some(m));
set_phase.set(AuthPhase::Authed);
}
Err(e) => {
set_error.set(e.to_string());
set_busy.set(false);
}
},
Err(e) => {
set_error.set(e.to_string());
set_busy.set(false);
}
}
});
};
view! {
<div class="center-screen">
<div class="card auth-card">
<h1>"filebrowser-ng"</h1>
<p class="muted">
"First boot: create the admin account. This user can manage
other users and server settings."
</p>
<form on:submit=on_submit>
<label class="field">
<span>"Name"</span>
<input
id="setup-name"
type="text"
autofocus=true
autocomplete="username"
/>
</label>
<label class="field">
<span>"Password"</span>
<input
id="setup-pass"
type="password"
autocomplete="new-password"
/>
</label>
<label class="field">
<span>"Repeat password"</span>
<input
id="setup-confirm"
type="password"
autocomplete="new-password"
/>
</label>
<p class="error">{move || error.get()}</p>
<button class="btn-primary" type="submit" disabled=move || busy.get()>
"Create admin account"
</button>
</form>
</div>
</div>
}
}
▾Aweb/src/views/shell.rs
@@ -0,0 +1,98 @@
use leptos::prelude::*;
use wasm_bindgen_futures::spawn_local;
use crate::api::{self, Me};
use crate::app::AuthPhase;
#[component]
pub fn ShellView(
me: ReadSignal<Option<Me>>,
set_me: WriteSignal<Option<Me>>,
set_phase: WriteSignal<AuthPhase>,
) -> impl IntoView {
let on_logout = move |_| {
spawn_local(async move {
let _ = api::logout().await;
set_me.set(None);
set_phase.set(AuthPhase::Login);
});
};
view! {
<div class="layout">
<header class="topbar">
<div class="brand">"📂 filebrowser-ng"</div>
<div class="topbar-right">
{move || {
let Some(m) = me.get() else {
return None;
};
let u = m.user?;
let is_admin = u.is_admin;
Some(
view! {
<span class="user-badge">
{u.name}
<Show when=move || is_admin>
<span class="badge">"admin"</span>
</Show>
</span>
}
.into_view()
.into_any(),
)
}}
<button class="btn" on:click=on_logout>"Log out"</button>
</div>
</header>
<main class="content">
<div class="card welcome-card">
{move || {
let Some(m) = me.get() else {
return None;
};
let u = m.user?;
Some(view! { "Welcome, " {u.name} "!" }.into_view().into_any())
}}
<p class="muted">
"You're signed in — auth, sessions and your configured
folders are working. The file browser itself arrives in
the next milestone."
</p>
<h2 class="section-title">"Your folders"</h2>
{move || {
let Some(m) = me.get() else {
return None;
};
Some(
view! {
<ul class="roots">
{m.roots.iter().map(|r| {
let name = r.name.clone();
let path = r.path.clone();
let mode = r.mode.clone();
view! {
<li>
<span class="root-name">{name}</span>
<span class="muted">{path}</span>
<span class="badge">
{if mode == "rw" {
"read-write"
} else {
"read-only"
}}
</span>
</li>
}
}).collect::<Vec<_>>()}
</ul>
}
.into_view()
.into_any(),
)
}}
</div>
</main>
</div>
}
}