diff --git a/Cargo.lock b/Cargo.lock index a36657f..8070ede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -82,6 +88,15 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "argon2" version = "0.5.3" @@ -128,9 +143,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -138,14 +153,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -323,15 +339,15 @@ checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bytesize" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" +checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e" [[package]] name = "cc" -version = "1.2.65" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -542,6 +558,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -613,6 +638,17 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "diff" version = "0.1.13" @@ -814,6 +850,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "flume" version = "0.11.1" @@ -906,6 +952,8 @@ dependencies = [ "tower_governor", "tracing", "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", "uuid", "validator", ] @@ -1037,9 +1085,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -1460,6 +1510,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -1549,11 +1601,11 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] @@ -1606,14 +1658,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "bitflags", "libc", "plain", - "redox_syscall 0.8.1", + "redox_syscall 0.9.0", ] [[package]] @@ -1706,6 +1758,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.1" @@ -1788,9 +1850,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2133,15 +2195,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.15" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -2155,16 +2218,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2211,9 +2274,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2264,6 +2327,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -2284,9 +2356,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ "bitflags", ] @@ -2403,6 +2475,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2 0.10.9", + "walkdir", +] + [[package]] name = "rust-multipart-rfc7578_2" version = "0.9.0" @@ -2414,15 +2520,15 @@ dependencies = [ "futures-util", "http", "mime", - "rand 0.10.1", + "rand 0.10.2", "thiserror 2.0.18", ] [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2474,9 +2580,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -2710,6 +2816,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "simd_cesu8" version = "1.1.1" @@ -3133,9 +3245,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "num-conv", @@ -3153,9 +3265,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -3546,6 +3658,48 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "9.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" +dependencies = [ + "axum", + "base64", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "1.23.4" @@ -3844,15 +3998,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -3886,30 +4031,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "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", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3922,12 +4050,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3940,12 +4062,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3958,24 +4074,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3988,12 +4092,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4006,12 +4104,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4024,12 +4116,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4042,12 +4128,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "wit-bindgen" version = "0.57.1" @@ -4169,8 +4249,40 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5431d5661c32445236631278f27946e444ddafe4684cac70b185272d4f9c52d5" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index a06e19d..5d7203f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ tower-http = { version = "0.6.6", features = ["cors", "limit", "trace", "fs"] } tower_governor = "0.8.0" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } +utoipa = { version = "5.5.0", features = ["uuid"] } +utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } uuid = { version = "1.19.0", features = ["serde", "v7"] } validator = "0.20.0" diff --git a/README.md b/README.md index a3370cb..2a713a7 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,16 @@ ## Configuration - Specify the JWT secret with the `FRANGIPANE_JWT_SECRET` environment variable. + +## AI usage + +Here is a list of things written by AI in this project: +- All the Utoipa annotations for the Swagger documentation +- File upload logic +- Voice chat audio handling + +Everything else is human-made. + +## Documentation + +The API serves a Swagger UI at `/swagger-ui`. diff --git a/flake.lock b/flake.lock index 93543e3..5764855 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768127708, - "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", + "lastModified": 1783224372, + "narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", + "rev": "d407951447dcd00442e97087bf374aad70c04cea", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1768272338, - "narHash": "sha256-Tg/kL8eKMpZtceDvBDQYU8zowgpr7ucFRnpP/AtfuRM=", + "lastModified": 1783320166, + "narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "03dda130a8701b08b0347fcaf850a190c53a3c1e", + "rev": "20ee15370c9256669d66968b89ee20a4b0a4e673", "type": "github" }, "original": { diff --git a/src/auth.rs b/src/auth.rs index ee18e60..f108128 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -7,7 +7,7 @@ use password_hash::rand_core::OsRng; use axum::{Json, http::HeaderMap}; use uuid::Uuid; -use crate::errors::APIError; +use crate::errors::{APIError, ErrorResponse}; const DEFAULT_SECRET_KEY: &str = "43aaf85b92f1ae6fbcef7732c50a0904"; @@ -83,6 +83,17 @@ pub fn verify_jwt_string(token: &String) -> Result { .map_err(|_| APIError::InvalidToken) } +#[utoipa::path( + get, + path = "/validate-token", + responses( + (status = 200, description = "Token is valid", body = serde_json::Value), + (status = 401, description = "Missing or invalid token", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] pub async fn validate_token(headers: HeaderMap) -> Result, APIError> { let _ = verify_jwt(headers)?; Ok(Json(serde_json::json!({"valid": true}))) diff --git a/src/errors.rs b/src/errors.rs index 1862c99..02cdf70 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -9,6 +9,12 @@ use serde_json::json; use crate::{MAX_ATTACHMENTS_PER_MESSAGE, MAX_ROOM_NAME_LENGTH, MAX_USERNAME_LENGTH}; +#[derive(serde::Serialize, utoipa::ToSchema)] +pub struct ErrorResponse { + pub code: String, + pub message: String, +} + #[derive(Debug)] pub enum APIError { // Auth Errors diff --git a/src/lib.rs b/src/lib.rs index 87e2521..6cda0da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,8 @@ use tower_http::{ trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}, }; use tracing::Level; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; const MAX_USERNAME_LENGTH: usize = 35; const MAX_ROOM_NAME_LENGTH: usize = 35; @@ -62,6 +64,92 @@ pub struct Cli { pub verbose: bool, } +/// Define the OpenAPI specification document structure +#[derive(OpenApi)] +#[openapi( + paths( + auth::validate_token, + routes::friends::list_friends, + routes::friends::list_requests, + routes::friends::send_request, + routes::friends::accept_request, + routes::friends::decline_request, + routes::friends::is_friend, + routes::friends::remove_friend, + routes::messages::list_messages, + routes::messages::create_message, + routes::messages::message_ws_handler, + routes::messages::create_message_with_attachments, + routes::messages::get_upload, + routes::rooms::list_rooms, + routes::rooms::create_room, + routes::rooms::get_room, + routes::rooms::list_invites, + routes::rooms::send_invite, + routes::rooms::accept_request, + routes::rooms::decline_request, + routes::rooms::leave_room, + routes::rooms::transfer_ownership, + routes::rooms::list_members, + routes::rooms::delete_room, + routes::users::login, + routes::users::register_user, + routes::users::update_user, + routes::users::upload_avatar, + routes::users::get_avatar, + routes::voice::voice_ws_handler, + routes::ws::issue_ws_token, + ), + components( + schemas( + errors::ErrorResponse, + routes::friends::Friend, + routes::friends::FriendRequest, + routes::friends::SendFriendRequestPayload, + routes::friends::AcceptFriendRequestPayload, + routes::friends::DeclineFriendRequestPayload, + routes::friends::RemoveFriendPayload, + routes::messages::Message, + routes::messages::Attachment, + routes::messages::NewMessagePayload, + routes::rooms::Room, + routes::rooms::NewRoomPayload, + routes::rooms::RoomInvite, + routes::rooms::SendRoomInvitePayload, + routes::rooms::AcceptRoomInvitePayload, + routes::rooms::TransferOwnershipPayload, + routes::users::User, + routes::users::UserProfile, + routes::users::LoginPayload, + routes::users::LoginResponse, + routes::users::NewUserPayload, + routes::users::UpdateUserPayoad, + routes::users::UpdateUserResponse, + routes::ws::WsAuthQuery, + ) + ), + modifiers(&SecurityAddon) +)] +pub struct ApiDoc; + +struct SecurityAddon; + +impl utoipa::Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + components.add_security_scheme( + "bearer_auth", + utoipa::openapi::security::SecurityScheme::Http( + utoipa::openapi::security::HttpBuilder::new() + .scheme(utoipa::openapi::security::HttpAuthScheme::Bearer) + .bearer_format("JWT") + .build(), + ), + ); + } + } +} + /// Create the app with all routes, middleware and extensions pub fn create_app( db_pool: sqlx::PgPool, @@ -99,6 +187,7 @@ pub fn create_app( .allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE]); let mut router = Router::new() + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) .route("/version", get(get_version)) .merge(routes::users::routes()) .merge(routes::rooms::routes()) diff --git a/src/routes/friends.rs b/src/routes/friends.rs index b057123..4e9eaa5 100644 --- a/src/routes/friends.rs +++ b/src/routes/friends.rs @@ -10,37 +10,37 @@ use uuid::Uuid; use crate::{auth::verify_jwt, db::id_from_username}; use crate::{ db::{user_id_from_uuid, username_from_id, username_from_uuid}, - errors::APIError, + errors::{APIError, ErrorResponse}, }; -#[derive(sqlx::FromRow, serde::Serialize)] +#[derive(sqlx::FromRow, serde::Serialize, utoipa::ToSchema)] pub struct Friend { pub uuid: Uuid, pub username: String, } -#[derive(sqlx::FromRow, serde::Serialize)] +#[derive(sqlx::FromRow, serde::Serialize, utoipa::ToSchema)] pub struct FriendRequest { pub sender_uuid: Uuid, pub sender_username: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct SendFriendRequestPayload { pub receiver_username: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct AcceptFriendRequestPayload { pub sender_uuid: Uuid, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct DeclineFriendRequestPayload { pub sender_uuid: Uuid, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct RemoveFriendPayload { pub friend_uuid: Uuid, } @@ -56,6 +56,19 @@ pub fn routes() -> Router { .route("/friends/remove", post(remove_friend)) } +#[utoipa::path( + get, + path = "/friends", + responses( + (status = 200, description = "Friends list successfully retrieved", body = [Friend]), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 500, description = "Internal server error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn list_friends( headers: HeaderMap, Extension(db): Extension, @@ -80,6 +93,19 @@ async fn list_friends( Ok(Json(friends)) } +#[utoipa::path( + get, + path = "/friends/requests", + responses( + (status = 200, description = "Friend requests list successfully retrieved", body = [FriendRequest]), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 500, description = "Internal server error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn list_requests( headers: HeaderMap, Extension(db): Extension, @@ -103,6 +129,22 @@ async fn list_requests( Ok(Json(requests)) } +#[utoipa::path( + post, + path = "/friends/request", + request_body = SendFriendRequestPayload, + responses( + (status = 201, description = "Friend request sent successfully", body = FriendRequest), + (status = 400, description = "Cannot send friend request to yourself", body = ErrorResponse), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 409, description = "You are already friends or Request already pending", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn send_request( headers: HeaderMap, Extension(db): Extension, @@ -151,6 +193,21 @@ async fn send_request( )) } +#[utoipa::path( + post, + path = "/friends/accept", + request_body = AcceptFriendRequestPayload, + responses( + (status = 201, description = "Friend request accepted successfully", body = Friend), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found or Friend request not found", body = ErrorResponse), + (status = 409, description = "You are already friends", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn accept_request( headers: HeaderMap, Extension(db): Extension, @@ -204,6 +261,20 @@ async fn accept_request( )) } +#[utoipa::path( + post, + path = "/friends/decline", + request_body = DeclineFriendRequestPayload, + responses( + (status = 201, description = "Friend request declined successfully"), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found or Friend request not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn decline_request( headers: HeaderMap, Extension(db): Extension, @@ -234,6 +305,22 @@ async fn decline_request( Ok(StatusCode::CREATED) } +#[utoipa::path( + get, + path = "/friends/check/{uuid}", + params( + ("uuid" = Uuid, Path, description = "Target user UUID to check friendship status") + ), + responses( + (status = 200, description = "Friendship status checked successfully", body = bool), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn is_friend( headers: HeaderMap, Path(target_uuid): Path, @@ -261,6 +348,20 @@ async fn is_friend( Ok(Json(is_friend)) } +#[utoipa::path( + post, + path = "/friends/remove", + request_body = RemoveFriendPayload, + responses( + (status = 200, description = "Friend removed successfully"), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found or User is not in your friends list", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn remove_friend( headers: HeaderMap, Extension(db): Extension, diff --git a/src/routes/messages.rs b/src/routes/messages.rs index 3749632..edb9b18 100644 --- a/src/routes/messages.rs +++ b/src/routes/messages.rs @@ -21,7 +21,7 @@ use crate::{ AppConfig, auth::{verify_jwt, verify_jwt_string}, db::room_id_from_uuid, - errors::APIError, + errors::{APIError, ErrorResponse}, routes::{rooms::is_member, ws::WsAuthQuery}, }; use crate::{ @@ -40,7 +40,7 @@ pub struct MessageRow { pub sent_at: chrono::NaiveDateTime, } -#[derive(serde::Serialize, Debug, Clone, serde::Deserialize)] +#[derive(serde::Serialize, Debug, Clone, serde::Deserialize, utoipa::ToSchema)] pub struct Message { pub uuid: Uuid, pub room_uuid: Uuid, @@ -52,20 +52,20 @@ pub struct Message { pub sent_at: String, } -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, utoipa::ToSchema)] pub struct Attachment { pub uuid: Uuid, pub file_type: String, // image, video, textfile, binaryfile pub file_name: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct NewMessagePayload { pub message_type: String, pub content: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::IntoParams)] struct MessageFetchQuery { limit: Option, before: Option, @@ -84,6 +84,24 @@ pub fn routes() -> Router { } /// Also resets `last_read_at` +#[utoipa::path( + get, + path = "/messages/{room_uuid}", + params( + ("room_uuid" = Uuid, Path, description = "UUID of the room to retrieve messages from"), + MessageFetchQuery + ), + responses( + (status = 200, description = "Messages successfully retrieved", body = [Message]), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn list_messages( Path(room_uuid): Path, Query(query): Query, @@ -103,7 +121,6 @@ async fn list_messages( let mut tx = db.begin().await?; - // 1. Fetch the messages (Simple query) let rows = sqlx::query_as::<_, MessageRow>( r#" SELECT @@ -129,11 +146,8 @@ async fn list_messages( .fetch_all(&mut *tx) .await?; - // 2. Collect all the UUIDs of the messages we just found let message_uuids: Vec = rows.iter().map(|m| m.uuid).collect(); - // 3. Fetch all attachments for these specific messages in one batch - // We use a temporary struct to capture the message_uuid link from the DB #[derive(sqlx::FromRow)] struct AttachmentRow { uuid: Uuid, @@ -149,7 +163,6 @@ async fn list_messages( .fetch_all(&mut *tx) .await?; - // 4. Group the attachments by their message_uuid in a HashMap let mut attachment_map: HashMap> = HashMap::new(); for att in attachment_rows { attachment_map @@ -162,7 +175,6 @@ async fn list_messages( }); } - // 5. Map the MessageRow to the final Message struct, pulling from the HashMap let mut messages: Vec = rows .into_iter() .map(|m| { @@ -174,7 +186,7 @@ async fn list_messages( sender_uuid: m.sender_uuid, message_type: m.message_type, content: m.content, - attachments, // Populated from HashMap + attachments, sent_at: m.sent_at.format("%Y-%m-%d %H:%M:%S").to_string(), } }) @@ -182,7 +194,7 @@ async fn list_messages( messages.reverse(); - // Reset last_read_at + // Reset `last_read_at` sqlx::query( "UPDATE membership_ SET last_read_at = CURRENT_TIMESTAMP WHERE user_id = $1 AND room = $2", ) @@ -196,6 +208,24 @@ async fn list_messages( Ok(Json(messages)) } +#[utoipa::path( + post, + path = "/messages/{room_uuid}", + params( + ("room_uuid" = Uuid, Path, description = "UUID of the room to create the message in") + ), + request_body = NewMessagePayload, + responses( + (status = 201, description = "Message created successfully", body = Message), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn create_message( Path(room_uuid): Path, Extension(db): Extension, @@ -261,6 +291,18 @@ async fn create_message( Ok((StatusCode::CREATED, Json(message))) } +#[utoipa::path( + get, + path = "/ws/messages", + params( + WsAuthQuery + ), + responses( + (status = 101, description = "WebSocket upgrade handshake accepted and connection initiated"), + (status = 401, description = "Invalid or expired token", body = ErrorResponse), + (status = 500, description = "Internal server error", body = ErrorResponse) + ) +)] async fn message_ws_handler( ws: WebSocketUpgrade, user_agent: Option>, @@ -284,8 +326,10 @@ async fn message_ws_handler( .bind(query.token) .execute(&db) .await - // NOTE: Maybe wrong type of error - .map_err(|e| APIError::Internal(format!("Failed to get WS token from DB: {e}")))?; + .map_err(|e| { + tracing::debug!("Failed to get WS token from DB: {e}"); + APIError::Internal(format!("Failed to get WS token from DB")) + })?; if result.rows_affected() == 0 { return Err(APIError::InvalidToken); @@ -362,6 +406,26 @@ async fn handle_message_socket( } } +#[utoipa::path( + post, + path = "/messages/{room_uuid}/upload", + params( + ("room_uuid" = Uuid, Path, description = "UUID of the room to upload attachments to") + ), + request_body(content = String, content_type = "multipart/form-data", description = "Multipart form with message payload fields: content and files"), + responses( + (status = 201, description = "Message with attachments created successfully", body = Message), + (status = 400, description = "Wrong file format or Maximum 8 files allowed", body = ErrorResponse), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 413, description = "One or more files exceed the size limit", body = ErrorResponse), + (status = 500, description = "Internal server error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn create_message_with_attachments( Path(room_uuid): Path, Extension(db): Extension, @@ -507,6 +571,17 @@ async fn create_message_with_attachments( Ok((StatusCode::CREATED, Json(message))) } +#[utoipa::path( + get, + path = "/uploads/{file_uuid}", + params( + ("file_uuid" = Uuid, Path, description = "UUID of the file attachment") + ), + responses( + (status = 200, description = "File served successfully"), + (status = 500, description = "File not found in DB, missing on disk, or file service error", body = ErrorResponse) + ) +)] async fn get_upload( Path(file_uuid): Path, Extension(config): Extension>, diff --git a/src/routes/rooms.rs b/src/routes/rooms.rs index acc5943..e77391d 100644 --- a/src/routes/rooms.rs +++ b/src/routes/rooms.rs @@ -10,10 +10,11 @@ use uuid::Uuid; use crate::{MAX_ROOM_NAME_LENGTH, auth::verify_jwt, db::room_id_from_uuid, errors::APIError}; use crate::{ db::{id_from_username, room_name_from_uuid, user_id_from_uuid, username_from_id}, + errors::ErrorResponse, routes::users::UserProfile, }; -#[derive(sqlx::FromRow, serde::Serialize, serde::Deserialize)] +#[derive(sqlx::FromRow, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] pub struct Room { pub uuid: Uuid, pub name: String, @@ -23,13 +24,13 @@ pub struct Room { pub unread_count: i64, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct NewRoomPayload { pub name: String, pub global: bool, } -#[derive(sqlx::FromRow, serde::Serialize)] +#[derive(sqlx::FromRow, serde::Serialize, utoipa::ToSchema)] pub struct RoomInvite { pub room_uuid: Uuid, pub room_name: String, @@ -37,19 +38,19 @@ pub struct RoomInvite { pub sender_username: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct SendRoomInvitePayload { pub room_uuid: Uuid, pub receiver_username: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct AcceptRoomInvitePayload { pub room_uuid: Uuid, pub sender_uuid: Uuid, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct TransferOwnershipPayload { pub room_uuid: Uuid, pub new_owner_uuid: Uuid, @@ -90,6 +91,19 @@ pub async fn is_member(user_id: i32, room_id: i32, db: &Pool) -> bool .unwrap_or(false) } +#[utoipa::path( + get, + path = "/rooms", + responses( + (status = 200, description = "Successfully listed rooms", body = [Room]), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn list_rooms( headers: HeaderMap, Extension(db): Extension, @@ -130,6 +144,21 @@ async fn list_rooms( Ok(Json(rooms)) } +#[utoipa::path( + post, + path = "/rooms", + request_body = NewRoomPayload, + responses( + (status = 201, description = "Room successfully created", body = Room), + (status = 400, description = "Room name must be 0-35 characters long", body = ErrorResponse), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn create_room( Extension(db): Extension, headers: HeaderMap, @@ -184,6 +213,23 @@ async fn create_room( )) } +#[utoipa::path( + get, + path = "/rooms/{room_uuid}", + params( + ("room_uuid" = Uuid, Path, description = "UUID of the room to retrieve") + ), + responses( + (status = 200, description = "Room successfully retrieved", body = Room), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn get_room( Path(room_uuid): Path, headers: HeaderMap, @@ -246,6 +292,19 @@ async fn get_room( })) } +#[utoipa::path( + get, + path = "/rooms/invites", + responses( + (status = 200, description = "List of room invites successfully retrieved", body = [RoomInvite]), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn list_invites( headers: HeaderMap, Extension(db): Extension, @@ -273,6 +332,22 @@ async fn list_invites( Ok(Json(requests)) } +#[utoipa::path( + post, + path = "/rooms/invite", + request_body = SendRoomInvitePayload, + responses( + (status = 201, description = "Room invite successfully sent", body = RoomInvite), + (status = 400, description = "Cannot invite yourself", body = ErrorResponse), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 409, description = "User is already a member or Invite already sent", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn send_invite( headers: HeaderMap, Extension(db): Extension, @@ -327,6 +402,21 @@ async fn send_invite( )) } +#[utoipa::path( + post, + path = "/rooms/join", + request_body = AcceptRoomInvitePayload, + responses( + (status = 201, description = "Room invite successfully accepted", body = Room), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found, Room not found, or Invite not found", body = ErrorResponse), + (status = 409, description = "User is already a member", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn accept_request( headers: HeaderMap, Extension(db): Extension, @@ -399,6 +489,20 @@ async fn accept_request( )) } +#[utoipa::path( + post, + path = "/rooms/decline", + request_body = AcceptRoomInvitePayload, + responses( + (status = 201, description = "Room invite successfully declined"), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found or Invite not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn decline_request( headers: HeaderMap, Extension(db): Extension, @@ -428,6 +532,23 @@ async fn decline_request( Ok(StatusCode::CREATED) } +#[utoipa::path( + post, + path = "/rooms/{room_uuid}/leave", + params( + ("room_uuid" = Uuid, Path, description = "UUID of the room to leave") + ), + responses( + (status = 200, description = "Successfully left the room"), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room or Owner cannot leave the room without transferring ownership", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn leave_room( headers: HeaderMap, Path(room_uuid): Path, @@ -469,6 +590,21 @@ async fn leave_room( Ok(StatusCode::OK) } +#[utoipa::path( + post, + path = "/rooms/transfer-ownership", + request_body = TransferOwnershipPayload, + responses( + (status = 200, description = "Room ownership successfully transferred"), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn transfer_ownership( headers: HeaderMap, Extension(db): Extension, @@ -515,6 +651,23 @@ async fn transfer_ownership( Ok(StatusCode::OK) } +#[utoipa::path( + get, + path = "/rooms/{room_uuid}/members", + params( + ("room_uuid" = Uuid, Path, description = "UUID of the room to list members of") + ), + responses( + (status = 200, description = "Room members successfully retrieved", body = [UserProfile]), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room or Cannot list members for global rooms", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn list_members( headers: HeaderMap, Path(room_uuid): Path, @@ -554,6 +707,23 @@ async fn list_members( Ok((StatusCode::OK, Json(members))) } +#[utoipa::path( + delete, + path = "/rooms/{room_uuid}/delete", + params( + ("room_uuid" = Uuid, Path, description = "UUID of the room to delete") + ), + responses( + (status = 200, description = "Room successfully deleted"), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn delete_room( headers: HeaderMap, Path(room_uuid): Path, diff --git a/src/routes/users.rs b/src/routes/users.rs index 4c498df..ecf06d7 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -15,10 +15,10 @@ use crate::{ AppConfig, MAX_UPLOAD_SIZE, MAX_USERNAME_LENGTH, auth::{create_jwt, hash_password, validate_token, verify_jwt, verify_password}, db::{user_id_from_uuid, username_from_uuid}, - errors::APIError, + errors::{APIError, ErrorResponse}, }; -#[derive(sqlx::FromRow, serde::Serialize)] +#[derive(sqlx::FromRow, serde::Serialize, utoipa::ToSchema)] pub struct User { pub uuid: Uuid, pub username: String, @@ -26,19 +26,19 @@ pub struct User { pub email: String, } -#[derive(sqlx::FromRow, serde::Serialize)] +#[derive(sqlx::FromRow, serde::Serialize, utoipa::ToSchema)] pub struct UserProfile { pub uuid: Uuid, pub username: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct LoginPayload { pub email: String, pub password: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)] pub struct LoginResponse { pub uuid: Uuid, pub username: String, @@ -46,21 +46,21 @@ pub struct LoginResponse { pub token: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct NewUserPayload { pub email: String, pub username: String, pub password: String, } -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, utoipa::ToSchema)] pub struct UpdateUserPayoad { pub email: String, pub username: String, pub password: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)] pub struct UpdateUserResponse { pub email: String, pub username: String, @@ -88,6 +88,17 @@ async fn registration_guard( Ok(next.run(req).await) } +#[utoipa::path( + post, + path = "/login", + request_body = LoginPayload, + responses( + (status = 200, description = "Logged in successfully", body = LoginResponse), + (status = 401, description = "Invalid email or password", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ) +)] pub async fn login( Extension(db): Extension, Json(payload): Json, @@ -123,6 +134,17 @@ pub async fn login( })) } +#[utoipa::path( + post, + path = "/register", + request_body = NewUserPayload, + responses( + (status = 201, description = "User successfully registered", body = LoginResponse), + (status = 400, description = "Required fields are empty, Invalid email format, Username must be 1-35 characters long, or Password must be at least 8 characters", body = ErrorResponse), + (status = 409, description = "Username already taken or Email already in use", body = ErrorResponse), + (status = 500, description = "Database error or Internal error", body = ErrorResponse) + ) +)] pub async fn register_user( Extension(db): Extension, Json(payload): Json, @@ -186,6 +208,22 @@ pub async fn register_user( )) } +#[utoipa::path( + put, + path = "/account/settings", + request_body = UpdateUserPayoad, + responses( + (status = 201, description = "Account settings successfully updated", body = UpdateUserResponse), + (status = 400, description = "Required fields are empty, Invalid email format, or Password must be at least 8 characters", body = ErrorResponse), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 409, description = "Username already taken or Email already in use", body = ErrorResponse), + (status = 500, description = "Database error or Internal error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] pub async fn update_user( headers: HeaderMap, Extension(db): Extension, @@ -249,6 +287,21 @@ pub async fn update_user( )) } +#[utoipa::path( + post, + path = "/account/upload-avatar", + request_body(content = [u8], content_type = "image/*", description = "Binary payload of raw avatar image"), + responses( + (status = 200, description = "Avatar successfully uploaded"), + (status = 400, description = "Wrong file format", body = ErrorResponse), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 404, description = "User not found", body = ErrorResponse), + (status = 500, description = "Database error or Internal error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] async fn upload_avatar( headers: HeaderMap, Extension(db): Extension, @@ -314,6 +367,18 @@ async fn upload_avatar( } // Public route +#[utoipa::path( + get, + path = "/account/get-avatar/{uuid}", + params( + ("uuid" = Uuid, Path, description = "UUID of the user whose avatar image is requested") + ), + responses( + (status = 200, description = "Avatar image served successfully"), + (status = 404, description = "Avatar not found", body = ErrorResponse), + (status = 500, description = "Internal error", body = ErrorResponse) + ) +)] async fn get_avatar( Path(uuid): Path, Extension(config): Extension>, diff --git a/src/routes/voice.rs b/src/routes/voice.rs index 346f0ad..13f1ee6 100644 --- a/src/routes/voice.rs +++ b/src/routes/voice.rs @@ -16,7 +16,7 @@ use uuid::Uuid; use crate::{ auth::verify_jwt_string, db::{room_id_from_uuid, user_id_from_uuid}, - errors::APIError, + errors::{APIError, ErrorResponse}, realtime::RealTimeVoices, routes::{rooms::is_member, ws::WsAuthQuery}, }; @@ -25,6 +25,21 @@ pub fn routes() -> Router { Router::new().route("/ws/voice/{room_uuid}", get(voice_ws_handler)) } +#[utoipa::path( + get, + path = "/ws/voice/{room_uuid}", + params( + ("room_uuid" = Uuid, Path, description = "UUID of the room voice channel to join"), + WsAuthQuery + ), + responses( + (status = 101, description = "WebSocket voice channel upgrade handshake accepted"), + (status = 401, description = "Invalid or expired token", body = ErrorResponse), + (status = 403, description = "You are not a member of this room", body = ErrorResponse), + (status = 404, description = "User not found or Room not found", body = ErrorResponse), + (status = 500, description = "Internal server error", body = ErrorResponse) + ) +)] async fn voice_ws_handler( ws: WebSocketUpgrade, Path(room_uuid): Path, diff --git a/src/routes/ws.rs b/src/routes/ws.rs index a2de3a5..b83a6a7 100644 --- a/src/routes/ws.rs +++ b/src/routes/ws.rs @@ -5,9 +5,9 @@ use axum::{Extension, http::StatusCode}; use serde::Deserialize; use crate::auth::{create_jwt, verify_jwt}; -use crate::errors::APIError; +use crate::errors::{APIError, ErrorResponse}; -#[derive(sqlx::FromRow, serde::Serialize, Deserialize)] +#[derive(sqlx::FromRow, serde::Serialize, Deserialize, utoipa::IntoParams, utoipa::ToSchema)] pub struct WsAuthQuery { pub token: String, } @@ -16,6 +16,18 @@ pub fn routes() -> axum::Router { axum::Router::new().route("/ws/issue-token", get(issue_ws_token)) } +#[utoipa::path( + get, + path = "/ws/issue-token", + responses( + (status = 201, description = "WebSocket authentication token successfully issued", body = WsAuthQuery), + (status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse), + (status = 500, description = "Database error", body = ErrorResponse) + ), + security( + ("bearer_auth" = []) + ) +)] pub async fn issue_ws_token( Extension(db): Extension, headers: HeaderMap,