add auth, FLCL reference

but I don't like sour drinks
This commit is contained in:
Butter 2026-04-20 09:37:09 -04:00
parent 44f1c1980c
commit 5d64a3137a
12 changed files with 832 additions and 11 deletions

1
.env
View File

@ -1 +1,2 @@
DATABASE_URL=sqlite:./sarmentine.db DATABASE_URL=sqlite:./sarmentine.db
SESSION_SECRET=gzC0+m1ol9sPHaZpEU/AwIQVA9STkmxrWK6HhCI7ovlNdVzQqR0sg8VHQCRxURQFmVNEiQvJ8cx7G7nNd3b4jw

421
Cargo.lock generated
View File

@ -21,12 +21,33 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]] [[package]]
name = "askama" name = "askama"
version = "0.12.1" version = "0.12.1"
@ -175,6 +196,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.8.3" version = "1.8.3"
@ -199,6 +226,15 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -208,6 +244,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -236,12 +278,48 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"base64 0.22.1",
"hmac",
"percent-encoding",
"rand",
"sha2",
"subtle",
"time",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@ -302,6 +380,16 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
"serde_core",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -411,6 +499,20 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.32" version = "0.3.32"
@ -455,6 +557,17 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.32" version = "0.3.32"
@ -475,6 +588,7 @@ checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr", "memchr",
@ -705,6 +819,30 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.2.0" version = "2.2.0"
@ -832,6 +970,16 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -901,6 +1049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
"serde",
] ]
[[package]] [[package]]
@ -990,6 +1139,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.46" version = "0.1.46"
@ -1049,6 +1204,17 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[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]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
@ -1118,6 +1284,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -1215,6 +1387,25 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "rmp"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"num-traits",
]
[[package]]
name = "rmp-serde"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"rmp",
"serde",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.10" version = "0.9.10"
@ -1265,13 +1456,19 @@ name = "sarmentine"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2",
"askama", "askama",
"askama_axum", "askama_axum",
"axum", "axum",
"chrono",
"dotenvy", "dotenvy",
"rand_core",
"serde",
"sqlx", "sqlx",
"tokio", "tokio",
"tower-http", "tower-http",
"tower-sessions",
"tower-sessions-sqlx-store",
] ]
[[package]] [[package]]
@ -1474,6 +1671,7 @@ dependencies = [
"atoi", "atoi",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
"either", "either",
@ -1497,6 +1695,7 @@ dependencies = [
"smallvec", "smallvec",
"sqlformat", "sqlformat",
"thiserror", "thiserror",
"time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",
@ -1534,6 +1733,7 @@ dependencies = [
"sha2", "sha2",
"sqlx-core", "sqlx-core",
"sqlx-mysql", "sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn 1.0.109", "syn 1.0.109",
"tempfile", "tempfile",
@ -1548,10 +1748,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64 0.21.7",
"bitflags", "bitflags",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"crc", "crc",
"digest", "digest",
"dotenvy", "dotenvy",
@ -1579,6 +1780,7 @@ dependencies = [
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror",
"time",
"tracing", "tracing",
"whoami", "whoami",
] ]
@ -1590,9 +1792,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64 0.21.7",
"bitflags", "bitflags",
"byteorder", "byteorder",
"chrono",
"crc", "crc",
"dotenvy", "dotenvy",
"etcetera", "etcetera",
@ -1617,6 +1820,7 @@ dependencies = [
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror",
"time",
"tracing", "tracing",
"whoami", "whoami",
] ]
@ -1628,6 +1832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
dependencies = [ dependencies = [
"atoi", "atoi",
"chrono",
"flume", "flume",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -1639,6 +1844,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"serde", "serde",
"sqlx-core", "sqlx-core",
"time",
"tracing", "tracing",
"url", "url",
"urlencoding", "urlencoding",
@ -1739,6 +1945,37 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@ -1832,6 +2069,23 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tower-cookies"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d"
dependencies = [
"async-trait",
"axum-core",
"cookie",
"futures-util",
"http",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.5.2" version = "0.5.2"
@ -1869,6 +2123,71 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tower-sessions"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50571505955aaa8b73f2f40489953d92b4d7ff9eb9b2a8b4e11fee0dcdb2760e"
dependencies = [
"async-trait",
"http",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core",
"tower-sessions-memory-store",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6293bf33f1977d5ef422c2e02f909eb2c3d7bf921d93557c40d4f1b130b84aa4"
dependencies = [
"async-trait",
"axum-core",
"base64 0.22.1",
"futures",
"http",
"parking_lot",
"rand",
"serde",
"serde_json",
"thiserror",
"time",
"tokio",
"tracing",
]
[[package]]
name = "tower-sessions-memory-store"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cec5f88eeef0f036e6900217034efbce733cbdf0528a85204eaaed90bc34c354"
dependencies = [
"async-trait",
"time",
"tokio",
"tower-sessions-core",
]
[[package]]
name = "tower-sessions-sqlx-store"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9442116d8ec67af57e2213f5b4007b6bb55d74c19eae429cd6525b7527844807"
dependencies = [
"async-trait",
"rmp-serde",
"sqlx",
"thiserror",
"time",
"tower-sessions-core",
]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
@ -2023,6 +2342,51 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.117",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wasm-encoder" name = "wasm-encoder"
version = "0.244.0" version = "0.244.0"
@ -2067,12 +2431,65 @@ dependencies = [
"wasite", "wasite",
] ]
[[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.117",
]
[[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.117",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View File

@ -9,7 +9,13 @@ tokio = { version = "1", features = ["full"] }
askama = { version = "0.12", features = ["with-axum"] } askama = { version = "0.12", features = ["with-axum"] }
askama_axum = "0.4" askama_axum = "0.4"
tower-http = { version = "0.5", features = ["fs"] } tower-http = { version = "0.5", features = ["fs"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "migrate"] } sqlx = { version = "0.7", features = ["runtime-tokio","chrono", "sqlite", "migrate"] }
dotenvy = "0.15" dotenvy = "0.15"
argon2 = "0.5"
rand_core = { version = "0.6", features = ["std"] }
tower-sessions = { version = "0.12", features = ["signed"] }
tower-sessions-sqlx-store = { version = "0.12", features = ["sqlite"] }
serde = { version = "1", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
# Error handling # Error handling
anyhow = "1" anyhow = "1"

172
src/handlers/auth.rs Normal file
View File

@ -0,0 +1,172 @@
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use askama::Template;
use axum::{
extract::{Form, State},
response::{Html, IntoResponse, Redirect, Response},
};
use serde::Deserialize;
use sqlx::SqlitePool;
use tower_sessions::Session;
use crate::models::user::User;
const SESSION_USER_ID_KEY: &str = "user_id";
// ~~ Templates
#[derive(Template)]
#[template(path = "auth/register.html")]
pub struct RegisterTemplate {
pub title: String,
pub current_user: Option<crate::CurrentUser>,
pub error: Option<String>,
}
#[derive(Template)]
#[template(path = "auth/login.html")]
pub struct LoginTemplate {
pub title: String,
pub current_user: Option<crate::CurrentUser>,
pub error: Option<String>,
}
fn render<T: Template>(t: T) -> Response {
match t.render() {
Ok(html) => Html(html).into_response(),
Err(_) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "render error").into_response(),
}
}
// ~~ Form types
#[derive(Deserialize)]
pub struct RegisterForm {
pub username: String,
pub email: String,
pub password: String,
}
#[derive(Deserialize)]
pub struct LoginForm {
pub username: String,
pub password: String,
}
// ~~ Handlers
pub async fn register_page() -> Response {
render(RegisterTemplate {
title: "register".into(),
current_user: None,
error: None,
})
}
pub async fn register_submit(
State(pool): State<SqlitePool>,
session: Session,
Form(form): Form<RegisterForm>,
) -> Response {
macro_rules! register_err {
($msg:expr) => {
return render(RegisterTemplate {
title: "register".into(),
current_user: None,
error: Some($msg.into()),
})
};
}
if form.username.len() < 2 || form.username.len() > 32 {
register_err!("username must be between 2 and 32 characters");
}
if form.password.len() < 8 {
register_err!("password must be at least 8 characters");
}
let existing_user: Option<User> = User::find_by_username(&pool, &form.username)
.await
.unwrap_or(None);
if existing_user.is_some() {
register_err!("that username is already taken");
}
let existing_email: Option<User> = User::find_by_email(&pool, &form.email)
.await
.unwrap_or(None);
if existing_email.is_some() {
register_err!("an account with that email already exists");
}
let salt = SaltString::generate(&mut OsRng);
let hash = Argon2::default()
.hash_password(form.password.as_bytes(), &salt)
.expect("password hashing failed")
.to_string();
let user_id: i64 = User::insert(&pool, &form.username, &form.email, &hash)
.await
.expect("failed to insert user");
session.insert(SESSION_USER_ID_KEY, user_id).await.unwrap();
Redirect::to("/").into_response()
}
pub async fn login_page() -> Response {
render(LoginTemplate {
title: "login".into(),
current_user: None,
error: None,
})
}
pub async fn login_submit(
State(pool): State<SqlitePool>,
session: Session,
Form(form): Form<LoginForm>,
) -> Response {
let fail = || {
render(LoginTemplate {
title: "login".into(),
current_user: None,
error: Some("incorrect username or password".into()),
})
};
let user: Option<User> = User::find_by_username(&pool, &form.username)
.await
.unwrap_or(None);
let user = match user {
Some(u) => u,
None => return fail(),
};
let parsed_hash = PasswordHash::new(&user.password).expect("invalid stored hash");
if Argon2::default()
.verify_password(form.password.as_bytes(), &parsed_hash)
.is_err()
{
return fail();
}
session.insert(SESSION_USER_ID_KEY, user.id).await.unwrap();
Redirect::to("/").into_response()
}
pub async fn logout(session: Session) -> Response {
session.flush().await.unwrap();
Redirect::to("/auth/login").into_response()
}
// ~~ Session helper
pub async fn get_current_user(session: &Session, pool: &SqlitePool) -> Option<User> {
let user_id: i64 = session.get(SESSION_USER_ID_KEY).await.ok()??;
User::find_by_id(pool, user_id).await.unwrap_or(None)
}

1
src/handlers/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod auth;

View File

@ -1,10 +1,22 @@
mod db; mod db;
mod handlers;
mod models;
use askama::Template; use askama::Template;
use askama_axum::IntoResponse; use axum::{
use axum::{routing::get, Router}; extract::State,
response::{Html, IntoResponse, Response},
routing::{get, post},
Router,
};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tower_sessions::{cookie::SameSite, SessionManagerLayer};
use tower_sessions_sqlx_store::SqliteStore;
use handlers::auth::{
get_current_user, login_page, login_submit, logout, register_page, register_submit,
};
pub struct CurrentUser { pub struct CurrentUser {
pub username: String, pub username: String,
@ -17,10 +29,21 @@ struct IndexTemplate {
current_user: Option<CurrentUser>, current_user: Option<CurrentUser>,
} }
async fn index() -> impl IntoResponse { async fn index(
IndexTemplate { State(pool): State<SqlitePool>,
session: tower_sessions::Session,
) -> Response {
let user: Option<models::user::User> = get_current_user(&session, &pool).await;
let current_user = user.map(|u| CurrentUser { username: u.username });
let tmpl = IndexTemplate {
title: "home".into(), title: "home".into(),
current_user: None, current_user,
};
match tmpl.render() {
Ok(html) => Html(html).into_response(),
Err(_) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "render error").into_response(),
} }
} }
@ -33,9 +56,28 @@ async fn main() {
let pool: SqlitePool = db::connect(&database_url).await; let pool: SqlitePool = db::connect(&database_url).await;
let session_store = SqliteStore::new(pool.clone());
session_store.migrate().await.expect("session store migration failed");
let session_secret = std::env::var("SESSION_SECRET")
.unwrap_or_else(|_| "change-me-in-production-use-a-long-random-string!!".into());
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false)
.with_same_site(SameSite::Lax)
.with_signed(tower_sessions::cookie::Key::from(
session_secret.as_bytes(),
));
let app = Router::new() let app = Router::new()
.route("/", get(index)) .route("/", get(index))
.route("/auth/register", get(register_page))
.route("/auth/register", post(register_submit))
.route("/auth/login", get(login_page))
.route("/auth/login", post(login_submit))
.route("/auth/logout", post(logout))
.nest_service("/static", ServeDir::new("static")) .nest_service("/static", ServeDir::new("static"))
.layer(session_layer)
.with_state(pool); .with_state(pool);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")

1
src/models/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod user;

70
src/models/user.rs Normal file
View File

@ -0,0 +1,70 @@
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct User {
pub id: i64,
pub username: String,
pub email: String,
#[serde(skip_serializing)]
pub password: String,
pub bio: Option<String>,
pub avatar_url: Option<String>,
pub role: String,
pub created_at: NaiveDateTime,
}
impl User {
pub async fn find_by_id(pool: &SqlitePool, id: i64) -> sqlx::Result<Option<User>> {
sqlx::query_as(
"SELECT id, username, email, password, bio, avatar_url, role, created_at
FROM users WHERE id = ?"
)
.bind(id)
.fetch_optional(pool)
.await
}
pub async fn find_by_username(pool: &SqlitePool, username: &str) -> sqlx::Result<Option<User>> {
sqlx::query_as(
"SELECT id, username, email, password, bio, avatar_url, role, created_at
FROM users WHERE username = ?"
)
.bind(username)
.fetch_optional(pool)
.await
}
pub async fn find_by_email(pool: &SqlitePool, email: &str) -> sqlx::Result<Option<User>> {
sqlx::query_as(
"SELECT id, username, email, password, bio, avatar_url, role, created_at
FROM users WHERE email = ?"
)
.bind(email)
.fetch_optional(pool)
.await
}
pub async fn insert(
pool: &SqlitePool,
username: &str,
email: &str,
password_hash: &str,
) -> sqlx::Result<i64> {
let result = sqlx::query(
"INSERT INTO users (username, email, password) VALUES (?, ?, ?)"
)
.bind(username)
.bind(email)
.bind(password_hash)
.execute(pool)
.await?;
Ok(result.last_insert_rowid())
}
pub fn is_admin(&self) -> bool {
self.role == "admin"
}
}

View File

@ -254,7 +254,7 @@ pre {
height: calc(31px * 2); height: calc(31px * 2);
} }
/* nav right (login/logout) */ /* ── nav right (login/logout) ───────────────────────────── */
.nav { .nav {
justify-content: flex-start; justify-content: flex-start;
} }
@ -285,7 +285,7 @@ pre {
background: #d4879c; background: #d4879c;
} }
/* layout: no sidebar */ /* ── layout: no sidebar ─────────────────────────────────── */
.layout { .layout {
display: block; display: block;
} }
@ -293,3 +293,48 @@ pre {
.main { .main {
max-width: 100%; max-width: 100%;
} }
/* ── forms ──────────────────────────────────────────────── */
.form-field {
margin-bottom: 10px;
}
.form-label {
display: block;
font-family: 'Silkscreen', monospace;
font-size: 10px;
color: #b48ead;
text-transform: uppercase;
margin-bottom: 4px;
}
.form-input {
width: 100%;
background: #fdf6e3;
border: 2px solid #b48ead;
color: #4a4060;
font-family: 'VT323', monospace;
font-size: 18px;
padding: 4px 8px;
outline: none;
}
.form-input:focus {
border-color: #d4879c;
}
.form-submit {
background: #4a4060;
color: #fdf6e3;
border: 2px solid #4a4060;
font-family: 'Silkscreen', monospace;
font-size: 11px;
padding: 6px 14px;
cursor: pointer;
margin-top: 6px;
}
.form-submit:hover {
background: #d4879c;
border-color: #d4879c;
}

31
templates/auth/login.html Normal file
View File

@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block content %}
<div class="site-title">login</div>
<div class="site-subtitle">:: i'm a computer ::</div>
<hr class="divider">
{% if let Some(err) = error %}
<div class="post" style="border-color:#d4879c; margin-bottom:12px;">
<div class="post-body" style="color:#d4879c;">⚠ {{ err }}</div>
</div>
{% endif %}
<div class="post">
<form method="post" action="/auth/login">
<div class="form-field">
<label class="form-label">username</label>
<input class="form-input" type="text" name="username" required autocomplete="username">
</div>
<div class="form-field">
<label class="form-label">password</label>
<input class="form-input" type="password" name="password" required autocomplete="current-password">
</div>
<button class="form-submit" type="submit">login →</button>
</form>
<div class="post-meta" style="margin-top:10px;">
new here? <a href="/auth/register" style="color:#d4879c;">register here</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block content %}
<div class="site-title">register</div>
<div class="site-subtitle">:: no more free gabagool ::</div>
<hr class="divider">
{% if let Some(err) = error %}
<div class="post" style="border-color:#d4879c; margin-bottom:12px;">
<div class="post-body" style="color:#d4879c;">⚠ {{ err }}</div>
</div>
{% endif %}
<div class="post">
<form method="post" action="/auth/register">
<div class="form-field">
<label class="form-label">username</label>
<input class="form-input" type="text" name="username" required minlength="2" maxlength="32" autocomplete="username">
</div>
<div class="form-field">
<label class="form-label">email</label>
<input class="form-input" type="email" name="email" required autocomplete="email">
</div>
<div class="form-field">
<label class="form-label">password</label>
<input class="form-input" type="password" name="password" required minlength="8" autocomplete="new-password">
</div>
<button class="form-submit" type="submit">register →</button>
</form>
<div class="post-meta" style="margin-top:10px;">
already have an account? <a href="/auth/login" style="color:#d4879c;">login here</a>
</div>
</div>
{% endblock %}

View File

@ -35,7 +35,7 @@
</div> </div>
<div class="status-bar"> <div class="status-bar">
<span>rhizomatic queer ascendance</span> <span>but i don't like sour drinks</span>
<span>toasterdragon.com</span> <span>toasterdragon.com</span>
</div> </div>
</div> </div>