commit eb1fd6e2ff22f15445911dcd53d1a53a38c44a48 Author: Mats Rauhala Date: Tue Oct 6 08:24:08 2026 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb6c7b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +.direnv +.envrc +.env diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..717b6e1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1183 @@ +# 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 = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "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 = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[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 = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[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 = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[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 = "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 = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[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-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libsqlite3-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[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 = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[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 = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[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 = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rukosync" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "dotenvy", + "hmac", + "secrecy", + "serde", + "serde_json", + "sha2", + "tempfile", + "thiserror", + "tokio", + "tokio-rusqlite", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "rusqlite" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", + "uuid", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + +[[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_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.6", +] + +[[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_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 = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[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 = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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 = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "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.6", +] + +[[package]] +name = "tokio-rusqlite" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bce5670317c76f8505e7e5a8d655330a435fc35d3deaf756de8406db64a94e8c" +dependencies = [ + "crossbeam-channel", + "rusqlite", + "tokio", +] + +[[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.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "percent-encoding", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[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 = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "uuid" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +dependencies = [ + "getrandom", + "js-sys", + "serde_core", + "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 = "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.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..94c48a2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rukosync" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { version = "1.0.104", features = ["backtrace"] } +axum = { version = "0.8.9", features = ["macros"] } +dotenvy = "0.15.7" +hmac = "0.13.0" +secrecy = { version = "0.10.3", features = ["serde"] } +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +sha2 = "0.11.0" +tempfile = "3.27.0" +thiserror = "2.0.21" +tokio = { version = "1.53.2", features = ["full"] } +tokio-rusqlite = { version = "0.8.0", features = ["bundled", "uuid"] } +tower = { version = "0.5.3", features = ["tracing"] } +tower-http = { version = "0.7.1", features = ["trace", "request-id"] } +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +uuid = { version = "1.27.0", features = ["v4", "serde"] } diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..80ea8e0 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1791048980, + "narHash": "sha256-KgItSKML8Xvte0B7/uGnBDsYzSnnKHcOaiUbgWBXLXw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a7868a727837f3c09cee2ce0ca671c76b1589fed", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..70682b0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,80 @@ +{ + description = "Rust development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + # Read the file relative to the flake's root + overrides = (builtins.fromTOML (builtins.readFile (self + "/rust-toolchain.toml"))); + libPath = with pkgs; lib.makeLibraryPath [ + # load external libraries that you need in your rust project here + ]; + + mealie-cli = pkgs.rustPlatform.buildRustPackage { + pname = "mealie-cli"; + version = "0.5.0"; + src = self; + cargoLock = { + lockFile = ./Cargo.lock; + }; + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ ]; + }; + in + { + packages.default = mealie-cli; + packages.mealie-cli = mealie-cli; + + apps.default = flake-utils.lib.mkApp { + drv = mealie-cli; + }; + + devShells.default = pkgs.mkShell rec { + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = with pkgs; [ + clang + llvmPackages.bintools + rustup + ]; + + RUSTC_VERSION = overrides.toolchain.channel; + + # https://github.com/rust-lang/rust-bindgen#environment-variables + LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; + + shellHook = '' + export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin + export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/ + ''; + + # Add precompiled library to rustc search path + RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') [ + # add libraries here (e.g. pkgs.libvmi) + ]); + + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (buildInputs ++ nativeBuildInputs); + + + # Add glibc, clang, glib, and other headers to bindgen search path + BINDGEN_EXTRA_CLANG_ARGS = + # Includes normal include path + (builtins.map (a: ''-I"${a}/include"'') [ + # add dev libraries here (e.g. pkgs.libvmi.dev) + pkgs.glibc.dev + ]) + # Includes with special directory paths + ++ [ + ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"'' + ''-I"${pkgs.glib.dev}/include/glib-2.0"'' + ''-I${pkgs.glib.out}/lib/glib-2.0/include/'' + ]; + }; + } + ); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..70fe030 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,4 @@ +pub mod users; +pub mod syncs; + + diff --git a/src/api/syncs.rs b/src/api/syncs.rs new file mode 100644 index 0000000..a846e0a --- /dev/null +++ b/src/api/syncs.rs @@ -0,0 +1,79 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Context as _; +use axum::{Json, Router, extract::{Path, State}, response::{IntoResponse, Response}, routing}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use crate::{app::AppState, app_error::AppError, auth_user::AuthUser, sync::Document}; + + +pub fn syncs_routes() -> Router { + Router::new() + .route("/progress", routing::put(put_progress)) + .route("/progress/{document}", routing::get(get_progress)) +} + +#[derive(Debug,Deserialize)] +pub struct PutProgress { + pub document: String, + pub progress: String, + pub percentage: f32, + pub device: String, + pub device_id: Option, +} + +#[derive(Debug,Serialize)] +pub struct ProgressCreated { + document: String, + timestamp: i64, +} + +#[instrument(skip(st))] +#[axum::debug_handler] +pub async fn put_progress(State(st) : State, AuthUser{username} : AuthUser, Json(progress): Json) -> Result, AppError> { + tracing::info!("Storing a new progress"); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .context("System clock is before unix epoch")? + .as_secs() as i64; + let document = Document { + timestamp, + document_id: progress.document, + progress: progress.progress, + percentage: progress.percentage, + device: progress.device, + device_id: progress.device_id, + }; + st.sync.set_progress(&username, &document).await?; + Ok(Json(ProgressCreated{timestamp: document.timestamp, document: document.document_id})) +} + +#[derive(Debug, Serialize)] +pub struct Progress { + document: String, + progress: String, + percentage: f32, + device: String, + timestamp: i64, + device_id: Option, +} + +#[instrument(skip(st))] +#[axum::debug_handler] +pub async fn get_progress(State(st) : State, AuthUser{username} : AuthUser, Path(document): Path) -> Result { + let d = st.sync.get_progress(&username, &document).await?; + tracing::info!("{:?}", d); + match d { + None => Ok(Json(serde_json::json!({})).into_response()), + Some(d) => + Ok(Json(Progress{ + document: d.document_id, + progress: d.progress, + percentage: d.percentage, + device: d.device, + timestamp: d.timestamp, + device_id: d.device_id, + }).into_response()) + } +} diff --git a/src/api/users.rs b/src/api/users.rs new file mode 100644 index 0000000..4bd898b --- /dev/null +++ b/src/api/users.rs @@ -0,0 +1,57 @@ +use axum::{Json, Router, extract::State, routing}; +use secrecy::SecretString; +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use crate::{app::AppState, app_error::AppError, auth_user::AuthUser}; + +#[derive(Deserialize, Debug)] +pub struct CreateUser { + username: String, + password: String, +} + +#[derive(Serialize, Debug)] +pub struct UserCreated { + username: String +} + + + + +pub fn users_routes() -> Router { + Router::new() + .route("/create", routing::post(create_user)) + .route("/auth", routing::get(check_auth)) + .route("/me", routing::delete(delete_user)) +} + +#[instrument(skip_all)] +#[axum::debug_handler] +pub async fn create_user(State(st) : State, Json(create_user): Json) -> Result, AppError> { + let username = st.users + .create_user(&create_user.username, SecretString::from(create_user.password)) + .await?; + Ok(Json(UserCreated{username})) +} + +#[derive(Serialize, Debug)] +pub struct AuthResult { + authorized: String +} + +#[instrument(skip_all)] +#[axum::debug_handler] +pub async fn check_auth(_: State, _ : AuthUser) -> Result, AppError> { + Ok(Json(AuthResult{authorized: "OK".to_string()})) +} + +#[derive(Debug, Serialize)] +pub struct DeleteResult { deleted: bool } + +#[instrument(skip_all)] +#[axum::debug_handler] +pub async fn delete_user(State(app_state): State, AuthUser{username} : AuthUser) -> Result, AppError> { + app_state.users.delete_user(&username).await?; + Ok(Json(DeleteResult{deleted: true})) +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..573f965 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,22 @@ +use std::{path::Path, sync::Arc}; + +use axum::extract::FromRef; + +use crate::{config::Config, reader::Reader, sync::ProgressSync, users::Users, writer::Writer}; + +#[derive(Clone, FromRef)] +pub struct AppState { + pub users: Users, + pub sync: ProgressSync, +} + +impl AppState { + pub async fn new(db_path: &Path, config: &Config) -> anyhow::Result { + let writer = Arc::new(Writer::new(&db_path).await?); + let reader = Arc::new(Reader::new(&db_path).await?); + let users = Users::new(writer.clone(), reader.clone(), config.server_secret.clone()); + let sync = ProgressSync::new(writer.clone(), reader.clone()); + Ok(Self{users, sync}) + } +} + diff --git a/src/app_error.rs b/src/app_error.rs new file mode 100644 index 0000000..761fa82 --- /dev/null +++ b/src/app_error.rs @@ -0,0 +1,59 @@ +use axum::{Json, http::StatusCode, response::IntoResponse}; +use thiserror::Error; + +use crate::{sync::SyncError, users::UserError}; + + +// The AppError represents the different error types from upstream kosync server +#[derive(Error, Debug)] +pub enum AppError { + #[error("Username already taken")] + UsernameTaken, + #[error("Unauthorized")] + Unauthorized, + #[error("Account not found")] + AccountNotFound, + #[error(transparent)] + Internal(#[from] anyhow::Error), +} + +impl From for AppError { + fn from(value: UserError) -> Self { + match value { + UserError::UnknownError(error) => Self::Internal(error), + UserError::UserAlreadyExists => Self::UsernameTaken, + UserError::DatabaseFailure(error) => Self::Internal(error.into()), + } + } +} + +impl From for AppError { + fn from(value: SyncError) -> Self { + match value { + SyncError::DatabaseFailure(error) => Self::Internal(error.into()), + } + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + let (status, error_code, msg) = match self { + AppError::UsernameTaken => (StatusCode::PAYMENT_REQUIRED, 2002, "Username is already registered"), + AppError::Internal(error) => { + tracing::error!(err=?error, "Internal server error"); + (StatusCode::INTERNAL_SERVER_ERROR, 2000, "Internal server error") + }, + AppError::Unauthorized => { + (StatusCode::UNAUTHORIZED, 2001, "Unauthorized") + } + AppError::AccountNotFound => { + (StatusCode::UNAUTHORIZED, 2006, "Account not found") + } + }; + let body = serde_json::json!({ + "code": error_code, + "message": msg + }); + (status, Json(body)).into_response() + } +} diff --git a/src/auth_user.rs b/src/auth_user.rs new file mode 100644 index 0000000..361223b --- /dev/null +++ b/src/auth_user.rs @@ -0,0 +1,32 @@ +use axum::extract::{FromRef as _, FromRequestParts}; +use secrecy::SecretString; + +use crate::{app::AppState, app_error::AppError, users}; + +pub struct AuthUser { pub username: String } + +impl FromRequestParts for AuthUser +{ + type Rejection = AppError; + + async fn from_request_parts( + parts: &mut axum::http::request::Parts, + state: &AppState, + ) -> Result { + let app_state = AppState::from_ref(state); + let username = parts + .headers + .get("x-auth-user").and_then(|h| h.to_str().ok()) + .ok_or(AppError::Unauthorized)?; + let key = parts + .headers + .get("x-auth-key").and_then(|h| h.to_str().ok().map(SecretString::from)) + .ok_or(AppError::Unauthorized)?; + match app_state.users.authenticate_user(username, key).await? { + users::Authenticated::Authenticated(username) => Ok(AuthUser{ username }), + users::Authenticated::UserNotFound => Err(AppError::AccountNotFound), + users::Authenticated::InvalidSecret => Err(AppError::Unauthorized), + } + } +} + diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8ac1d4c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,25 @@ +use std::env; + +use secrecy::SecretString; +use thiserror::Error; + +pub struct Config { + pub server_secret: SecretString, +} + +#[derive(Error, Debug)] +pub enum ConfigError { + #[error("Missing SERVER_SECRET")] + MissingServerSecret, + #[error("Unknown error: #{0}")] + Unknown(anyhow::Error), +} + +impl Config { + pub fn new() -> Result { + dotenvy::dotenv().map_err(|err| ConfigError::Unknown(err.into()))?; + let server_secret = env::var("SERVER_SECRET").map(SecretString::from).map_err(|_| ConfigError::MissingServerSecret)?; + Ok(Self{server_secret}) + } +} + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..10ada9a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod config; +pub mod writer; +pub mod users; +pub mod sync; +pub mod reader; +pub mod api; +pub mod app; +pub mod auth_user; +pub mod app_error; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..182d55e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,39 @@ +use std::path::Path; + +use axum::{Json, Router, response::{IntoResponse as _, Response}, routing}; +use rukosync::{api::{syncs, users}, app::AppState, config::Config, }; +use tower_http::trace::TraceLayer; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _}; + + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let level_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + tracing_subscriber::registry() + .with(fmt::layer()) + .with(level_filter) + .init(); + let config = Config::new()?; + + let app_state = AppState::new(Path::new("/tmp/kosync.db"), &config).await?; + + tracing::info!("Hello"); + + let app = Router::new() + .route("/healthcheck", routing::get(healthcheck)) + .nest("/users", users::users_routes()) + .nest("/syncs", syncs::syncs_routes()) + .with_state(app_state) + .layer(TraceLayer::new_for_http()); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; + axum::serve(listener, app).await?; + Ok(()) +} + +#[axum::debug_handler] +async fn healthcheck() -> Response { + Json(serde_json::json!({"state": "OK"})).into_response() +} diff --git a/src/reader.rs b/src/reader.rs new file mode 100644 index 0000000..29de271 --- /dev/null +++ b/src/reader.rs @@ -0,0 +1,14 @@ +use std::path::Path; + +use tokio_rusqlite::{Connection, OpenFlags}; + +pub struct Reader { + pub connection: Connection, +} + +impl Reader { + pub async fn new(path: &Path) -> Result { + let connection = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY).await?; + Ok(Self{connection}) + } +} diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 0000000..37c8abb --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,205 @@ +use std::sync::Arc; + +use thiserror::Error; +use tokio_rusqlite::{OptionalExtension as _, named_params, rusqlite}; + +use crate::{reader::Reader, writer::Writer}; + +#[derive(Clone)] +pub struct ProgressSync { + writer: Arc, + reader: Arc, +} + + +#[derive(Error, Debug)] +pub enum SyncError { + #[error("Database call failed {0}")] + DatabaseFailure(#[from] tokio_rusqlite::Error) +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Document { + // document identifier, name of the file, md5 of the file or something + pub document_id: String, + // opaque progress + pub progress: String, + pub percentage: f32, + pub device: String, + pub device_id: Option, + pub timestamp: i64, +} + +impl ProgressSync { + pub fn new(writer: Arc, reader: Arc) -> Self { + Self{writer,reader} + } + + pub async fn set_progress(&self, username: &str, document: &Document) -> Result<(), SyncError> { + let username = username.to_owned(); + let doc = document.clone(); + self.writer.connection.call(move |conn| -> rusqlite::Result<()> { + conn.execute("insert into progress (user_id, document_id, progress, percentage, device, device_id, timestamp) + select id, :document_id, :progress, :percentage, :device, :device_id, :timestamp from users + where username = :username + on conflict (user_id, document_id) do update + set progress = excluded.progress, + percentage = excluded.percentage, + device = excluded.device, + device_id = excluded.device_id, + timestamp = excluded.timestamp", named_params! + { ":username": username, + ":document_id": doc.document_id, + ":progress": doc.progress, + ":percentage": doc.percentage, + ":device": doc.device, + ":device_id": doc.device_id, + ":timestamp": doc.timestamp, + })?; + Ok(()) + }).await?; + Ok(()) + } + + pub async fn get_progress(&self, username: &str, document_id: &str) -> Result, SyncError> { + let username = username.to_owned(); + let document_id = document_id.to_owned(); + let doc = self.reader.connection.call(move |conn| -> rusqlite::Result> { + let x : Option = conn.query_row("select + document_id, progress, percentage, device, device_id, timestamp + from progress + left join users on progress.user_id = users.id + where users.username = :username and progress.document_id = :document_id", named_params! + { ":username": username, + ":document_id": document_id, + }, |row| { + let document_id : String = row.get(0)?; + let progress : String = row.get(1)?; + let percentage : f32 = row.get(2)?; + let device : String = row.get(3)?; + let device_id : Option = row.get(4)?; + let timestamp : i64 = row.get(5)?; + Ok(Document{ document_id, progress, percentage, device, device_id, timestamp }) + }).optional()?; + Ok(x) + }).await?; + Ok(doc) + } +} + +#[cfg(test)] +mod tests { + use std::time::{SystemTime, UNIX_EPOCH}; + +use secrecy::SecretString; + use tempfile::{TempDir, tempdir}; + + use crate::users::Users; + + use super::*; + + async fn create_data() -> anyhow::Result<(TempDir, Users, ProgressSync)> { + let temp_dir = tempdir()?; + let path = temp_dir.path().join("kosync.db"); + let writer = Arc::new(Writer::new(&path).await?); + let reader = Arc::new(Reader::new(&path).await?); + let users = Users::new(writer.clone(), reader.clone(), SecretString::from("secret")); + let sync = ProgressSync::new(writer.clone(), reader.clone()); + Ok((temp_dir, users, sync)) + } + + fn now() -> anyhow::Result { + let n = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; + Ok(n) + } + + #[tokio::test] + async fn put_get_values() -> anyhow::Result<()> { + let (_t, users, sync) = create_data().await?; + + let username = "foo@example.com"; + let username = users.create_user(username, SecretString::from("password")).await?; + + let doc = Document { + document_id: "foo".to_string(), + progress: "prog".to_string(), + percentage: 0.5, + device: "kobo".to_string(), + device_id: None, + timestamp: now()?, + }; + + sync.set_progress(&username, &doc).await?; + let got = sync.get_progress(&username, "foo").await?; + + assert_eq!(got, Some(doc)); + Ok(()) + } + + #[tokio::test] + async fn put_gets_latest() -> anyhow::Result<()> { + let (_t, users, sync) = create_data().await?; + + let username = "foo@example.com"; + let username = users.create_user(username, SecretString::from("password")).await?; + let document_id = "foo".to_string(); + + let doc1 = Document { + document_id: document_id.clone(), + progress: "prog".to_string(), + percentage: 0.5, + device: "kobo".to_string(), + device_id: None, + timestamp: now()?, + }; + let doc2 = Document { + document_id: document_id.clone(), + progress: "prog2".to_string(), + percentage: 0.5, + device: "kobo".to_string(), + device_id: None, + timestamp: now()?, + }; + + sync.set_progress(&username, &doc1).await?; + sync.set_progress(&username, &doc2).await?; + let got = sync.get_progress(&username, &document_id).await?; + + assert_eq!(got, Some(doc2)); + Ok(()) + } + + #[tokio::test] + async fn put_gets_new_device() -> anyhow::Result<()> { + let (_t, users, sync) = create_data().await?; + + let username = "foo@example.com"; + let username = users.create_user(username, SecretString::from("password")).await?; + + let document_id = "foo".to_string(); + + let doc1 = Document { + document_id: document_id.clone(), + progress: "prog".to_string(), + percentage: 0.5, + device: "kobo".to_string(), + device_id: None, + timestamp: now()?, + }; + let doc2 = Document { + document_id: document_id.clone(), + progress: "prog2".to_string(), + percentage: 0.5, + device: "x4".to_string(), + device_id: None, + timestamp: now()? + 3, + }; + + sync.set_progress(&username, &doc1).await?; + sync.set_progress(&username, &doc2).await?; + let got = sync.get_progress(&username, &document_id).await?; + + assert_eq!(got, Some(doc2)); + Ok(()) + } +} diff --git a/src/users.rs b/src/users.rs new file mode 100644 index 0000000..bd4d076 --- /dev/null +++ b/src/users.rs @@ -0,0 +1,190 @@ +use std::sync::Arc; + +use hmac::{Hmac, KeyInit as _, Mac as _, digest::InvalidLength}; +use secrecy::{ExposeSecret as _, SecretBox, SecretSlice, SecretString}; +use sha2::Sha256; +use thiserror::Error; +use tokio_rusqlite::{OptionalExtension as _, rusqlite}; +use uuid::Uuid; + +use crate::{reader::Reader, writer::Writer}; + +#[derive(Clone)] +pub struct Users { + writer : Arc, + reader : Arc, + secret: SecretString +} + +type Hmac256 = Hmac; + +#[derive(Error, Debug)] +pub enum UserError { + #[error("Unknown error #{0}")] + UnknownError(anyhow::Error), + #[error("User already exists")] + UserAlreadyExists, + #[error("Database failure #{0}")] + DatabaseFailure(tokio_rusqlite::Error), + +} + +impl From for UserError { + fn from(value: InvalidLength) -> Self { + Self::UnknownError(value.into()) + } +} + +impl From for UserError { + fn from(value: tokio_rusqlite::Error) -> Self { + Self::DatabaseFailure(value) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub enum Authenticated { + Authenticated(String), + UserNotFound, + InvalidSecret +} + +impl Users { + pub fn new(writer: Arc, reader: Arc, secret: SecretString) -> Self { + Self{writer, reader, secret} + } + + fn create_secret(&self, password: &SecretString) -> Result, UserError> { + let mut mac = Hmac256::new_from_slice(self.secret.expose_secret().as_bytes())?; + mac.update(password.expose_secret().as_bytes()); + let secret = SecretSlice::from(mac.finalize().into_bytes().as_slice().to_vec()); + Ok(secret) + } + + pub async fn create_user(&self, username: &str, password: SecretString) -> Result { + let secret = self.create_secret(&password)?; + let username = username.to_owned(); + let created_username = self.writer.connection.call(move |conn| -> rusqlite::Result> { + let params = (Uuid::new_v4(), &username, secret.expose_secret()); + let inserted = conn.execute("insert into users (id, username, secret) values (?, ?, ?) on conflict(username) do nothing", params)?; + if inserted == 1 { Ok(Some(username)) } else { Ok(None) } + }).await?; + created_username.ok_or(UserError::UserAlreadyExists) + } + + + pub async fn authenticate_user(&self, username: &str, password: SecretString) -> Result { + let secret = self.create_secret(&password)?; + let username = username.to_owned(); + let authenticated = self.reader.connection.call(move |conn| -> rusqlite::Result { + let user = conn.query_row("select username, secret from users where username = ?", [&username], |row| { + let u : String = row.get(0)?; + let s : SecretBox<[u8]> = SecretSlice::from(row.get::<_, Box<[u8]>>(1)?); + Ok((u,s)) + }).optional()?; + match user { + None => Ok(Authenticated::UserNotFound), + Some((username, s)) => { + if secret.expose_secret() == s.expose_secret() + { Ok(Authenticated::Authenticated(username)) } + else { Ok(Authenticated::InvalidSecret) } + } + } + }).await?; + Ok(authenticated) + } + + pub async fn delete_user(&self, username: &str) -> Result<(), UserError> { + let username = username.to_owned(); + self.writer.connection.call(move |conn| -> rusqlite::Result<()> { + conn.execute("delete from users where username = ?", [username])?; + Ok(()) + }).await?; + Ok(()) + } + +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + + use super::*; + + async fn create_users() -> anyhow::Result<(TempDir, Users)> { + let temp_dir = tempdir()?; + let path = temp_dir.path().join("kosync.db"); + let writer = Arc::new(Writer::new(&path).await?); + let reader = Arc::new(Reader::new(&path).await?); + let users = Users::new(writer, reader, SecretString::from("secret")); + Ok((temp_dir, users)) + } + + #[tokio::test] + async fn create_user_returns_username() -> anyhow::Result<()> { + let (_t, users) = create_users().await?; + let username = "foo@example.com"; + let got = users.create_user(username, SecretString::from("password")).await?; + assert_eq!(username, got); + Ok(()) + } + + #[tokio::test] + async fn user_can_authenticate() -> anyhow::Result<()> { + let (_t, users) = create_users().await?; + let username = "foo@example.com"; + let password = SecretString::from("password"); + let _ = users.create_user(username, password.clone()).await?; + let auth = users.authenticate_user(username, password.clone()).await?; + let wanted = Authenticated::Authenticated(username.to_owned()); + std::assert_eq!(auth, wanted); + Ok(()) + } + + #[tokio::test] + async fn missing_user_not_found() -> anyhow::Result<()> { + let (_t, users) = create_users().await?; + let username = "foo@example.com"; + let password = SecretString::from("password"); + let _ = users.create_user(username, password.clone()).await?; + let auth = users.authenticate_user("bar@example.com", password.clone()).await?; + let wanted = Authenticated::UserNotFound; + std::assert_eq!(auth, wanted); + Ok(()) + } + + #[tokio::test] + async fn wrong_password_invalid() -> anyhow::Result<()> { + let (_t, users) = create_users().await?; + let username = "foo@example.com"; + let password = SecretString::from("password"); + let wrong_password = SecretString::from("passwordxyz"); + let _ = users.create_user(username, password.clone()).await?; + let auth = users.authenticate_user(username, wrong_password.clone()).await?; + let wanted = Authenticated::InvalidSecret; + std::assert_eq!(auth, wanted); + Ok(()) + } + + #[tokio::test] + async fn creating_user_twice_fails() -> anyhow::Result<()> { + let (_t, users) = create_users().await?; + let username = "foo@example.com"; + let _ = users.create_user(username, SecretString::from("password")).await?; + let got = users.create_user(username, SecretString::from("password")).await; + std::assert_matches!(got, Err(UserError::UserAlreadyExists)); + Ok(()) + } + + #[tokio::test] + async fn user_not_found_after_delete() -> anyhow::Result<()> { + let (_t, users) = create_users().await?; + let username = "foo@example.com"; + let password = SecretString::from("password"); + let _ = users.create_user(username, password.clone()).await?; + let _ = users.delete_user(username).await?; + let auth = users.authenticate_user(username, password.clone()).await?; + let wanted = Authenticated::UserNotFound; + std::assert_eq!(auth, wanted); + Ok(()) + } +} diff --git a/src/writer.rs b/src/writer.rs new file mode 100644 index 0000000..e6a8914 --- /dev/null +++ b/src/writer.rs @@ -0,0 +1,24 @@ +use std::path::Path; + +use tokio_rusqlite::{Connection, rusqlite}; + + +pub struct Writer { + // Note that I don't need to wrap this in a mutex because + // tokio_rusqlite already serializes each request into a single executor + pub connection: Connection, +} + +impl Writer { + pub async fn new(path: &Path) -> Result { + let connection = Connection::open(path).await?; + + connection.call(|conn| -> rusqlite::Result<()> { + conn.execute("create table if not exists users (id primary key, username not null, secret not null, unique(username))", [])?; + conn.execute("create table if not exists progress (user_id not null, document_id not null, progress not null, percentage not null, device not null, device_id, timestamp not null, unique(user_id, document_id), foreign key (user_id) references users(id))", [])?; + Ok(()) + }).await?; + + Ok(Self{connection}) + } +}