From 9f40f0fe2db3c7fc66fca40132aab734e90672e6 Mon Sep 17 00:00:00 2001 From: servius Date: Mon, 27 Jul 2026 07:51:36 +0530 Subject: [PATCH] feat: own SQLite session store; drop tower-sessions-sqlx-store, sqlx 0.9 Replace the unmaintained tower-sessions-sqlx-store (last release 2025-01, pinned to sqlx 0.8) with a small in-tree SqliteSessionStore implementing tower_sessions::SessionStore over the shared app-DB pool. Stores each session Record as a JSON blob with the expiry in an integer column; periodic ExpiredDeletion GC is wired in main.rs alongside the existing sync-point GC. All store queries are 'static literals. With the store crate gone, sqlx moves 0.8 -> 0.9. That required the 0.9 SqlSafeStr migration for dynamic query strings (AssertSqlSafe) and dropping the removed SqliteArguments lifetime parameter in book_store. tower-sessions stays 0.14 (still pinned by axum-login 0.18). nix flake check passes. --- Cargo.lock | 325 ++++++++++++++----------------------------- Cargo.toml | 11 +- src/db/app_db.rs | 22 +-- src/db/book_store.rs | 20 +-- src/lib.rs | 1 + src/main.rs | 20 ++- src/session_store.rs | 140 +++++++++++++++++++ 7 files changed, 287 insertions(+), 252 deletions(-) create mode 100644 src/session_store.rs diff --git a/Cargo.lock b/Cargo.lock index e4c15f9..d86d02b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -420,6 +420,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "color_quant" version = "1.1.0" @@ -460,7 +466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "base64 0.22.1", - "hmac", + "hmac 0.12.1", "percent-encoding", "rand 0.8.7", "sha2 0.10.9", @@ -563,6 +569,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -665,6 +680,7 @@ dependencies = [ "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -749,7 +765,7 @@ dependencies = [ "ff", "generic-array", "group", - "hkdf", + "hkdf 0.12.4", "pem-rfc7468", "pkcs8", "rand_core 0.6.4", @@ -795,13 +811,12 @@ dependencies = [ [[package]] name = "etcetera" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "home", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -859,9 +874,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ "futures-core", "futures-sink", @@ -870,9 +885,9 @@ dependencies = [ [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "form_urlencoded" @@ -1049,9 +1064,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", @@ -1066,11 +1081,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -1091,7 +1106,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", ] [[package]] @@ -1104,12 +1128,12 @@ dependencies = [ ] [[package]] -name = "home" -version = "0.5.12" +name = "hmac" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "windows-sys 0.61.2", + "digest 0.11.3", ] [[package]] @@ -1458,6 +1482,7 @@ dependencies = [ name = "kobors" version = "0.1.0" dependencies = [ + "async-trait", "axum", "axum-listener", "axum-login", @@ -1483,7 +1508,6 @@ dependencies = [ "tower", "tower-http 0.7.0", "tower-sessions", - "tower-sessions-sqlx-store", "tracing", "tracing-subscriber", "uuid", @@ -1511,23 +1535,11 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[package]] -name = "libredox" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "bitflags", - "libc", - "plain", - "redox_syscall 0.9.0", -] - [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -1587,6 +1599,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + [[package]] name = "memchr" version = "2.8.3" @@ -1759,7 +1781,7 @@ dependencies = [ "chrono", "dyn-clone", "ed25519-dalek", - "hmac", + "hmac 0.12.1", "http", "itertools", "log", @@ -1837,7 +1859,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -1913,12 +1935,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "png" version = "0.18.1" @@ -2131,15 +2147,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" -dependencies = [ - "bitflags", -] - [[package]] name = "ref-cast" version = "1.0.26" @@ -2221,7 +2228,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -2239,25 +2246,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[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]] name = "rsa" version = "0.9.10" @@ -2518,13 +2506,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.7" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2636,9 +2624,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2649,12 +2637,13 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ "base64 0.22.1", "bytes", + "cfg-if", "chrono", "crc", "crossbeam-queue", @@ -2664,19 +2653,17 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "hashlink", "indexmap 2.14.0", "log", "memchr", - "once_cell", "percent-encoding", "serde", "serde_json", "sha2 0.10.9", "smallvec", "thiserror 2.0.19", - "time", "tokio", "tokio-stream", "tracing", @@ -2686,9 +2673,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" dependencies = [ "proc-macro2", "quote", @@ -2699,15 +2686,15 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" dependencies = [ + "cfg-if", "dotenvy", "either", "heck", "hex", - "once_cell", "proc-macro2", "quote", "serde", @@ -2718,60 +2705,44 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.119", + "thiserror 2.0.19", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" dependencies = [ - "atoi", - "base64 0.22.1", "bitflags", "byteorder", "bytes", "chrono", "crc", - "digest 0.10.7", + "digest 0.11.3", "dotenvy", "either", - "futures-channel", "futures-core", - "futures-io", "futures-util", "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", "log", - "md-5", - "memchr", - "once_cell", "percent-encoding", - "rand 0.8.7", - "rsa", "serde", "sha1", - "sha2 0.10.9", - "smallvec", + "sha2 0.11.0", "sqlx-core", - "stringprep", "thiserror 2.0.19", - "time", "tracing", "uuid", - "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" dependencies = [ "atoi", "base64 0.22.1", @@ -2785,23 +2756,20 @@ dependencies = [ "futures-core", "futures-util", "hex", - "hkdf", - "hmac", - "home", + "hkdf 0.13.0", + "hmac 0.13.0", "itoa", "log", - "md-5", + "md-5 0.11.0", "memchr", - "once_cell", - "rand 0.8.7", + "rand 0.10.2", "serde", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "smallvec", "sqlx-core", "stringprep", "thiserror 2.0.19", - "time", "tracing", "uuid", "whoami", @@ -2809,13 +2777,14 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ "atoi", "chrono", "flume", + "form_urlencoded", "futures-channel", "futures-core", "futures-executor", @@ -2825,10 +2794,8 @@ dependencies = [ "log", "percent-encoding", "serde", - "serde_urlencoded", "sqlx-core", "thiserror 2.0.19", - "time", "tracing", "url", "uuid", @@ -3236,20 +3203,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-sessions-sqlx-store" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e054622079f57fc1a7d6a6089c9334f963d62028fe21dc9eddd58af9a78480b3" -dependencies = [ - "async-trait", - "rmp-serde", - "sqlx", - "thiserror 1.0.69", - "time", - "tower-sessions-core", -] - [[package]] name = "tracing" version = "0.1.44" @@ -3408,7 +3361,7 @@ checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom 0.4.3", "js-sys", - "md-5", + "md-5 0.10.6", "serde_core", "wasm-bindgen", ] @@ -3446,12 +3399,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -3544,13 +3491,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "whoami" -version = "1.6.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" [[package]] name = "windows-core" @@ -3611,22 +3554,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3638,67 +3572,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3711,48 +3612,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index fef23b7..51b862b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" # Database (SQLite for both Calibre and app DB) -# Held at 0.8: tower-sessions-sqlx-store 0.15 (the web session store) targets -# sqlx 0.8; sqlx 0.9 would pull a second sqlx and break the shared SqlitePool. -sqlx = { version = "0.8", features = [ +sqlx = { version = "0.9", features = [ "runtime-tokio", "sqlite", "chrono", @@ -79,13 +77,14 @@ reqwest = { version = "0.12", default-features = false, features = [ # Authentication / sessions axum-login = "0.18" -# Held at 0.14: tower-sessions-sqlx-store 0.15 (the SqliteStore) targets -# tower-sessions 0.14; 0.15 splits tower-sessions-core and breaks SqliteStore. +# Held at 0.14: axum-login 0.18 depends on tower-sessions 0.14. The SQLite +# session store is our own (src/session_store.rs) rather than the unmaintained +# tower-sessions-sqlx-store, which pinned sqlx to 0.8. tower-sessions = { version = "0.14", default-features = false, features = [ "axum-core", "signed", ] } -tower-sessions-sqlx-store = { version = "0.15", features = ["sqlite"] } +async-trait = "0.1" password-auth = "1.0" time = "0.3" openidconnect = { version = "4.0.1", default-features = false, features = [ diff --git a/src/db/app_db.rs b/src/db/app_db.rs index 07f5068..a07188b 100644 --- a/src/db/app_db.rs +++ b/src/db/app_db.rs @@ -340,7 +340,7 @@ impl AppDb { ]; for sql in &statements { - sqlx::query(sql) + sqlx::query(*sql) .execute(&self.pool) .await .change_context(Error::Database) @@ -373,9 +373,9 @@ impl AppDb { column: &str, definition: &str, ) -> Result<()> { - let exists = sqlx::query(&format!( + let exists = sqlx::query(sqlx::AssertSqlSafe(format!( "SELECT 1 FROM pragma_table_info('{table}') WHERE name = ?" - )) + ))) .bind(column) .fetch_optional(&self.pool) .await @@ -384,9 +384,9 @@ impl AppDb { .is_some(); if !exists { - sqlx::query(&format!( + sqlx::query(sqlx::AssertSqlSafe(format!( "ALTER TABLE {table} ADD COLUMN {column} {definition}" - )) + ))) .execute(&self.pool) .await .change_context(Error::Database) @@ -1250,7 +1250,7 @@ impl AppDb { reading_state_last_modified, is_archived, synced) VALUES {placeholders}" ); - let mut q = sqlx::query(&sql); + let mut q = sqlx::query(sqlx::AssertSqlSafe(sql)); for b in chunk { q = q .bind(sync_point_id) @@ -1342,7 +1342,7 @@ impl AppDb { VALUES {placeholders}" ); - let mut q = sqlx::query(&sql); + let mut q = sqlx::query(sqlx::AssertSqlSafe(sql)); for uuid in chunk { q = q.bind(sync_point_id).bind(shelf_id).bind(uuid); } @@ -1522,7 +1522,7 @@ impl AppDb { size: usize, ) -> Result<(Vec, bool)> { let limit_plus_one = (size as i64) + 1; - let rows = sqlx::query(sql) + let rows = sqlx::query(sqlx::AssertSqlSafe(sql)) .bind(bind1) .bind(bind2) .bind(limit_plus_one) @@ -1556,7 +1556,7 @@ impl AppDb { WHERE sync_point_id = ? AND book_id IN ({placeholders})" ); - let mut q = sqlx::query(&sql).bind(sync_point_id); + let mut q = sqlx::query(sqlx::AssertSqlSafe(sql)).bind(sync_point_id); for &id in chunk { q = q.bind(id); } @@ -1679,7 +1679,7 @@ impl AppDb { size: usize, ) -> Result<(Vec, bool)> { let limit_plus_one = (size as i64) + 1; - let rows = sqlx::query(sql) + let rows = sqlx::query(sqlx::AssertSqlSafe(sql)) .bind(bind1) .bind(bind2) .bind(limit_plus_one) @@ -1716,7 +1716,7 @@ impl AppDb { WHERE sync_point_id = ? AND shelf_id IN ({placeholders})" ); - let mut q = sqlx::query(&sql).bind(sync_point_id); + let mut q = sqlx::query(sqlx::AssertSqlSafe(sql)).bind(sync_point_id); for &id in chunk { q = q.bind(id); } diff --git a/src/db/book_store.rs b/src/db/book_store.rs index a15d6f9..359af9a 100644 --- a/src/db/book_store.rs +++ b/src/db/book_store.rs @@ -223,11 +223,13 @@ impl BookStore { /// Fetch all books in the catalog, ordered by title. pub async fn fetch_all_books(&self) -> Result> { - let rows = sqlx::query(&Self::select("ORDER BY title COLLATE NOCASE ASC")) - .fetch_all(&self.pool) - .await - .change_context(Error::Database) - .attach("Failed to fetch all books")?; + let rows = sqlx::query(sqlx::AssertSqlSafe(Self::select( + "ORDER BY title COLLATE NOCASE ASC", + ))) + .fetch_all(&self.pool) + .await + .change_context(Error::Database) + .attach("Failed to fetch all books")?; Ok(rows .iter() @@ -238,7 +240,7 @@ impl BookStore { /// Fetch every book paired with its preferred format + size. One row per /// book. Used to populate a sync-point snapshot. pub async fn fetch_syncable_books_with_format(&self) -> Result> { - let rows = sqlx::query(&Self::select("ORDER BY id ASC")) + let rows = sqlx::query(sqlx::AssertSqlSafe(Self::select("ORDER BY id ASC"))) .fetch_all(&self.pool) .await .change_context(Error::Database) @@ -432,7 +434,7 @@ impl BookStore { async fn fetch_row_by(&self, column: &str, value: &str) -> Result> { let sql = format!("{SELECT_COLUMNS} WHERE {column} = ? LIMIT 1"); - let row = sqlx::query(&sql) + let row = sqlx::query(sqlx::AssertSqlSafe(sql)) .bind(value) .fetch_optional(&self.pool) .await @@ -460,9 +462,9 @@ const INSERT_BOOK_SQL: &str = r#" /// Bind a [`NewBook`]'s fields onto the shared insert statement. fn bind_new_book<'q>( - query: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>>, + query: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments>, book: &'q NewBook, -) -> sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>> { +) -> sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments> { let authors = serde_json::to_string(&book.authors).unwrap_or_else(|_| "[]".to_string()); query .bind(book.id) diff --git a/src/lib.rs b/src/lib.rs index d02bb42..1b34b63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,6 @@ pub mod errors; pub mod ingest; pub mod kobo; pub mod oidc; +pub mod session_store; pub mod upload; pub mod web; diff --git a/src/main.rs b/src/main.rs index 6416bf5..055edd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,8 +8,8 @@ use clap::Parser; use time::Duration; use tower::Layer; use tower_http::trace::TraceLayer; +use tower_sessions::session_store::ExpiredDeletion as _; use tower_sessions::{Expiry, SessionManagerLayer}; -use tower_sessions_sqlx_store::SqliteStore; use tracing::{info, warn}; use tracing_subscriber::EnvFilter; @@ -23,6 +23,7 @@ use kobors::kobo::auth::AppState; use kobors::kobo::router::KoboRouter; use kobors::kobo::sync_point::gc_old_sync_points; use kobors::oidc::OidcProvider; +use kobors::session_store::SqliteSessionStore; use kobors::web; #[tokio::main] @@ -113,9 +114,24 @@ async fn main() -> Result<(), Box> { } // Build session layer (SQLite-backed, shares the app DB pool) - let session_store = SqliteStore::new(app_db.pool().clone()); + let session_store = SqliteSessionStore::new(app_db.pool().clone()); session_store.migrate().await?; + // Periodically purge expired web sessions from the store. + { + let store = session_store.clone(); + tokio::spawn(async move { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600)); + interval.tick().await; // immediate first tick — skip + loop { + interval.tick().await; + if let Err(e) = store.delete_expired().await { + tracing::warn!("Session GC failed: {e}"); + } + } + }); + } + let session_layer = SessionManagerLayer::new(session_store) .with_secure(false) .with_expiry(Expiry::OnInactivity(Duration::days(7))); diff --git a/src/session_store.rs b/src/session_store.rs new file mode 100644 index 0000000..b858f40 --- /dev/null +++ b/src/session_store.rs @@ -0,0 +1,140 @@ +//! SQLite-backed [`tower_sessions::SessionStore`] over the app database. +//! +//! Replaces the unmaintained `tower-sessions-sqlx-store` crate, which pinned +//! sqlx to 0.8. Web-login sessions live in the `tower_sessions` table of the +//! shared app-DB pool: the full [`Record`] is stored as a JSON blob, with its +//! expiry mirrored into an integer column (unix seconds) so expired rows can be +//! filtered on load and garbage-collected. +//! +//! All queries are `'static` string literals — no dynamic SQL, so nothing here +//! needs sqlx's `AssertSqlSafe` escape hatch. + +use async_trait::async_trait; +use sqlx::sqlite::SqlitePool; +use sqlx::Row; +use time::OffsetDateTime; +use tower_sessions::session::{Id, Record}; +use tower_sessions::session_store::{self, ExpiredDeletion}; +use tower_sessions::SessionStore; + +/// SQLite session store backed by the shared app-DB pool. +#[derive(Clone, Debug)] +pub struct SqliteSessionStore { + pool: SqlitePool, +} + +impl SqliteSessionStore { + /// Wrap an existing SQLite pool. + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + /// Create the sessions table if it does not exist. + pub async fn migrate(&self) -> sqlx::Result<()> { + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS tower_sessions ( + id TEXT PRIMARY KEY NOT NULL, + data BLOB NOT NULL, + expiry_date INTEGER NOT NULL + ) + "#, + ) + .execute(&self.pool) + .await?; + Ok(()) + } +} + +#[async_trait] +impl SessionStore for SqliteSessionStore { + async fn create(&self, record: &mut Record) -> session_store::Result<()> { + // Retry with a fresh random id on the (astronomically unlikely) id + // collision, mirroring the default-store contract. + loop { + let result = + sqlx::query("INSERT INTO tower_sessions (id, data, expiry_date) VALUES (?, ?, ?)") + .bind(record.id.to_string()) + .bind(encode(record)?) + .bind(record.expiry_date.unix_timestamp()) + .execute(&self.pool) + .await; + + match result { + Ok(_) => return Ok(()), + Err(sqlx::Error::Database(e)) if e.is_unique_violation() => { + record.id = Id::default(); + } + Err(e) => return Err(backend(e)), + } + } + } + + async fn save(&self, record: &Record) -> session_store::Result<()> { + sqlx::query( + r#" + INSERT INTO tower_sessions (id, data, expiry_date) VALUES (?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + data = excluded.data, + expiry_date = excluded.expiry_date + "#, + ) + .bind(record.id.to_string()) + .bind(encode(record)?) + .bind(record.expiry_date.unix_timestamp()) + .execute(&self.pool) + .await + .map_err(backend)?; + Ok(()) + } + + async fn load(&self, session_id: &Id) -> session_store::Result> { + let now = OffsetDateTime::now_utc().unix_timestamp(); + let row = sqlx::query("SELECT data FROM tower_sessions WHERE id = ? AND expiry_date > ?") + .bind(session_id.to_string()) + .bind(now) + .fetch_optional(&self.pool) + .await + .map_err(backend)?; + + match row { + // A row that fails to decode (e.g. written by the previous store's + // MessagePack format) is treated as no session rather than an error, + // so a stale cookie just prompts a fresh login. + Some(row) => Ok(serde_json::from_slice(&row.get::, _>("data")).ok()), + None => Ok(None), + } + } + + async fn delete(&self, session_id: &Id) -> session_store::Result<()> { + sqlx::query("DELETE FROM tower_sessions WHERE id = ?") + .bind(session_id.to_string()) + .execute(&self.pool) + .await + .map_err(backend)?; + Ok(()) + } +} + +#[async_trait] +impl ExpiredDeletion for SqliteSessionStore { + async fn delete_expired(&self) -> session_store::Result<()> { + let now = OffsetDateTime::now_utc().unix_timestamp(); + sqlx::query("DELETE FROM tower_sessions WHERE expiry_date < ?") + .bind(now) + .execute(&self.pool) + .await + .map_err(backend)?; + Ok(()) + } +} + +/// Serialize a session record to its stored JSON blob. +fn encode(record: &Record) -> session_store::Result> { + serde_json::to_vec(record).map_err(|e| session_store::Error::Encode(e.to_string())) +} + +/// Map a sqlx error into the session-store backend error. +fn backend(e: sqlx::Error) -> session_store::Error { + session_store::Error::Backend(e.to_string()) +} -- 2.51.2