Add image API

- Add is_admin field to User model.
- Add functions to get and validate current user.
- Improve error handling.
- Add routes to upload, view and delete images.
This commit is contained in:
Ivan R. 2024-11-05 19:46:41 +05:00
parent f65ae78eb9
commit 559d6a7d9e
Signed by: lumin
GPG key ID: 9B2CA5D12844D4D0
14 changed files with 871 additions and 59 deletions

583
Cargo.lock generated
View file

@ -38,6 +38,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "aligned-vec"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
[[package]]
name = "allocator-api2"
version = "0.2.18"
@ -114,12 +120,35 @@ version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
[[package]]
name = "arbitrary"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775a8770d29db3dadcb858482cc240af7b2ffde4ac4de67d1d4955728103f0e2"
[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-trait"
version = "0.1.83"
@ -152,6 +181,29 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "av1-grain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
dependencies = [
"anyhow",
"arrayvec",
"log",
"nom",
"num-rational",
"v_frame",
]
[[package]]
name = "avif-serialize"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62"
dependencies = [
"arrayvec",
]
[[package]]
name = "axum"
version = "0.7.7"
@ -172,6 +224,7 @@ dependencies = [
"matchit",
"memchr",
"mime",
"multer",
"percent-encoding",
"pin-project-lite",
"rustversion",
@ -275,6 +328,18 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
@ -284,6 +349,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bitstream-io"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452"
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -303,18 +374,36 @@ dependencies = [
"serde",
]
[[package]]
name = "built"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.8.0"
@ -327,9 +416,21 @@ version = "1.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-expr"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
"smallvec",
"target-lexicon",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -372,6 +473,12 @@ dependencies = [
"phf_codegen",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.3"
@ -389,6 +496,7 @@ dependencies = [
"chrono",
"config",
"env_logger",
"image",
"jsonwebtoken",
"lazy_static",
"log",
@ -402,6 +510,7 @@ dependencies = [
"tower",
"tower-http",
"url",
"uuid",
]
[[package]]
@ -518,6 +627,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
@ -700,12 +818,46 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "exr"
version = "1.73.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
dependencies = [
"bit_field",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fastrand"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "fdeflate"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.1"
@ -842,6 +994,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gif"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gimli"
version = "0.31.1"
@ -867,7 +1029,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"ignore",
"walkdir",
]
@ -891,6 +1053,16 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@ -1159,6 +1331,45 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "image"
version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"exr",
"gif",
"image-webp",
"num-traits",
"png",
"qoi",
"ravif",
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
name = "imgref"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "indexmap"
version = "2.6.0"
@ -1169,6 +1380,17 @@ dependencies = [
"hashbrown 0.15.0",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "ipnet"
version = "2.10.1"
@ -1181,12 +1403,36 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
[[package]]
name = "js-sys"
version = "0.3.72"
@ -1231,12 +1477,29 @@ dependencies = [
"spin",
]
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "libfuzzer-sys"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
dependencies = [
"arbitrary",
"cc",
"once_cell",
]
[[package]]
name = "libm"
version = "0.2.8"
@ -1276,12 +1539,31 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "loop9"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
dependencies = [
"imgref",
]
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "maybe-rayon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"rayon",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -1327,6 +1609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
@ -1341,6 +1624,23 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http",
"httparse",
"memchr",
"mime",
"spin",
"version_check",
]
[[package]]
name = "native-tls"
version = "0.2.12"
@ -1358,6 +1658,12 @@ dependencies = [
"tempfile",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nom"
version = "7.1.3"
@ -1368,6 +1674,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "num-bigint"
version = "0.4.6"
@ -1401,6 +1713,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-integer"
version = "0.1.46"
@ -1421,6 +1744,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1452,7 +1786,7 @@ version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"cfg-if",
"foreign-types",
"libc",
@ -1697,6 +2031,19 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "png"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -1721,6 +2068,40 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "profiling"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "1.0.37"
@ -1760,13 +2141,83 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rav1e"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
dependencies = [
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"interpolate_name",
"itertools",
"libc",
"libfuzzer-sys",
"log",
"maybe-rayon",
"new_debug_unreachable",
"noop_proc_macro",
"num-derive",
"num-traits",
"once_cell",
"paste",
"profiling",
"rand",
"rand_chacha",
"simd_helpers",
"system-deps",
"thiserror",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6"
dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error",
"rav1e",
"rayon",
"rgb",
]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags",
"bitflags 2.6.0",
]
[[package]]
@ -1841,6 +2292,12 @@ dependencies = [
"windows-registry",
]
[[package]]
name = "rgb"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
[[package]]
name = "ring"
version = "0.17.8"
@ -1863,7 +2320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [
"base64 0.21.7",
"bitflags",
"bitflags 2.6.0",
"serde",
"serde_derive",
]
@ -1910,7 +2367,7 @@ version = "0.38.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
@ -1998,7 +2455,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"core-foundation",
"core-foundation-sys",
"libc",
@ -2125,6 +2582,21 @@ dependencies = [
"rand_core",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_helpers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
"quote",
]
[[package]]
name = "simple_asn1"
version = "0.6.2"
@ -2261,6 +2733,7 @@ dependencies = [
"tokio-stream",
"tracing",
"url",
"uuid",
]
[[package]]
@ -2310,7 +2783,7 @@ checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags",
"bitflags 2.6.0",
"byteorder",
"bytes",
"chrono",
@ -2342,6 +2815,7 @@ dependencies = [
"stringprep",
"thiserror",
"tracing",
"uuid",
"whoami",
]
@ -2353,7 +2827,7 @@ checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags",
"bitflags 2.6.0",
"byteorder",
"chrono",
"crc",
@ -2381,6 +2855,7 @@ dependencies = [
"stringprep",
"thiserror",
"tracing",
"uuid",
"whoami",
]
@ -2406,6 +2881,7 @@ dependencies = [
"sqlx-core",
"tracing",
"url",
"uuid",
]
[[package]]
@ -2457,7 +2933,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"core-foundation",
"system-configuration-sys",
]
@ -2472,6 +2948,25 @@ dependencies = [
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml",
"version-compare",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tempfile"
version = "3.13.0"
@ -2527,6 +3022,17 @@ dependencies = [
"syn",
]
[[package]]
name = "tiff"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
dependencies = [
"flate2",
"jpeg-decoder",
"weezl",
]
[[package]]
name = "time"
version = "0.3.36"
@ -2712,7 +3218,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"bytes",
"futures-util",
"http",
@ -2912,12 +3418,39 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "v_frame"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
dependencies = [
"aligned-vec",
"num-traits",
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
[[package]]
name = "version_check"
version = "0.9.5"
@ -3032,6 +3565,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "weezl"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "whoami"
version = "1.5.2"
@ -3284,3 +3823,27 @@ name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768"
dependencies = [
"zune-core",
]

View file

@ -5,13 +5,13 @@ edition = "2021"
[dependencies]
tokio = { version = "1.41.0", features = ["full"] }
axum = { version = "0.7.7", features = [ "macros" ] }
axum = { version = "0.7.7", features = [ "macros", "multipart", "json" ] }
axum-extra = { version = "0.9.4", features = [ "cookie" ] }
tera = "1.20.0"
lazy_static = "1.5.0"
tower = "0.5.1"
tower-http = { version = "0.6.1", features = ["fs"] }
sqlx = { version = "0.8.2", features = [ "runtime-tokio", "postgres", "chrono" ] }
sqlx = { version = "0.8.2", features = [ "runtime-tokio", "postgres", "chrono", "uuid" ] }
url = { version = "2.5.2", features = [ "serde" ] }
serde = { version = "1.0.214", features = [ "derive" ] }
config = "0.14.1"
@ -24,3 +24,5 @@ rand = "0.8.5"
rand_core = { version = "0.6.4", features = [ "getrandom" ] }
base64 = "0.22.1"
chrono = "0.4.38"
uuid = { version = "1.11.0", features = [ "v4", "serde" ] }
image = "0.25.5"

View file

@ -1,6 +1,7 @@
use config::{Config, Environment, File};
use lazy_static::lazy_static;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Deserialize)]
pub struct OpenIdConfig {
@ -23,6 +24,7 @@ pub struct AppConfig {
pub url: url::Url,
/// Port for the web server.
pub port: u16,
pub uploads_dir: PathBuf,
pub openid: OpenIdConfig,
pub database: DatabaseConfig,
}

68
src/internal/db/images.rs Normal file
View file

@ -0,0 +1,68 @@
use sqlx::PgPool;
pub struct NewImage {
pub id: uuid::Uuid,
pub width: i32,
pub height: i32,
pub mime_type: String,
pub original_filename: Option<String>,
pub alt: Option<String>,
}
#[derive(sqlx::FromRow)]
pub struct Image {
pub id: uuid::Uuid,
pub width: i32,
pub height: i32,
pub mime_type: String,
pub original_filename: String,
pub alt: Option<String>,
pub uploaded_at: chrono::DateTime<chrono::Utc>,
}
pub async fn save_image(pool: &PgPool, img: NewImage) -> Result<(), sqlx::Error> {
sqlx::query(
"
INSERT INTO images(id, width, height, mime_type, original_filename, alt)
VALUES ($1, $2, $3, $4, $5, $6)
",
)
.bind(img.id)
.bind(img.width)
.bind(img.height)
.bind(img.mime_type)
.bind(img.original_filename)
.bind(img.alt)
.execute(pool)
.await?;
Ok(())
}
pub async fn get_image(pool: &PgPool, id: uuid::Uuid) -> Result<Image, sqlx::Error> {
sqlx::query_as::<_, Image>(
"
SELECT *
FROM images
WHERE id = $1
",
)
.bind(id)
.fetch_one(pool)
.await
}
pub async fn delete_image(pool: &PgPool, id: uuid::Uuid) -> Result<(), sqlx::Error> {
sqlx::query_as::<_, (i32,)>(
"
DELETE FROM images
WHERE id = $1
RETURNING 1
",
)
.bind(id)
.fetch_one(pool)
.await?;
Ok(())
}

View file

@ -0,0 +1,12 @@
CREATE TABLE images(
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
width INTEGER NOT NULL,
height INTEGER NOT NULL,
mime_type TEXT NOT NULL,
original_filename TEXT,
alt TEXT,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE users
ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE;

View file

@ -1,3 +1,4 @@
pub mod images;
pub mod users;
pub async fn apply_migrations(pool: &sqlx::PgPool) -> Result<(), sqlx::migrate::MigrateError> {

View file

@ -19,6 +19,7 @@ pub struct User {
pub username: String,
pub email: String,
pub display_name: String,
pub is_admin: bool,
}
/// Create a new user using external auth provider.
@ -162,9 +163,8 @@ pub async fn generate_openid_state(pool: &PgPool) -> Result<[u8; 16], anyhow::Er
}
/// Check state returned by openid authorization endpoint.
/// Returns true if provided state was found and deleted.
pub async fn delete_openid_state(pool: &PgPool, state: Vec<u8>) -> Result<bool, anyhow::Error> {
let res = sqlx::query_as::<_, (i32,)>(
pub async fn delete_openid_state(pool: &PgPool, state: Vec<u8>) -> Result<(), anyhow::Error> {
sqlx::query_as::<_, (i32,)>(
"
DELETE FROM tokens
WHERE context = $1
@ -174,8 +174,8 @@ pub async fn delete_openid_state(pool: &PgPool, state: Vec<u8>) -> Result<bool,
)
.bind(TOKEN_CONTEXT_OPENID)
.bind(state)
.fetch_optional(pool)
.fetch_one(pool)
.await?;
Ok(res.is_some())
Ok(())
}

View file

@ -37,6 +37,11 @@ async fn main() {
.route("/openid/signin", post(controllers::signin))
.route("/openid/exchange-code", get(controllers::exchange_code))
.route("/logout", post(controllers::logout))
.route("/api/images", post(controllers::upload_image))
.route(
"/api/images/:image_id",
get(controllers::get_image).delete(controllers::delete_image),
)
.nest_service("/assets", ServeDir::new("src/web/assets"))
.with_state(shared_state.clone());

View file

@ -1,17 +1,18 @@
use axum::{
extract::State,
http::header::SET_COOKIE,
http::{header::SET_COOKIE, StatusCode},
response::{AppendHeaders, IntoResponse, Redirect},
};
use axum_extra::extract::CookieJar;
use std::sync::Arc;
use super::ControllerError;
use crate::internal::db::users;
pub async fn logout(
cookies: CookieJar,
State(state): State<Arc<super::AppState>>,
) -> Result<impl IntoResponse, super::ControllerError> {
) -> Result<impl IntoResponse, ControllerError> {
let token = super::ctx::extract_auth_token(cookies)?;
match token {
@ -21,6 +22,10 @@ pub async fn logout(
let headers = AppendHeaders([(SET_COOKIE, "comfy-session=deleted; Max-Age=0;")]);
Ok((headers, Redirect::to("/")))
}
None => Err(anyhow::anyhow!("can't find session token").into()),
None => Err(ControllerError {
status_code: StatusCode::UNAUTHORIZED,
description: String::from("Can't find session token. Are you logged in?"),
details: None,
}),
}
}

View file

@ -1,9 +1,13 @@
use anyhow::Result;
use axum::http::StatusCode;
use axum_extra::extract::cookie::CookieJar;
use base64::prelude::*;
use sqlx::PgPool;
use crate::internal::db::users;
use super::ControllerError;
use crate::internal::db::users::{self, User};
pub fn extract_auth_token(cookies: CookieJar) -> Result<Option<Vec<u8>>, anyhow::Error> {
pub fn extract_auth_token(cookies: CookieJar) -> Result<Option<Vec<u8>>> {
let cookie = match cookies.get("comfy-session") {
Some(cookie) => cookie,
None => {
@ -15,10 +19,7 @@ pub fn extract_auth_token(cookies: CookieJar) -> Result<Option<Vec<u8>>, anyhow:
}
/// Get current user by looking at request cookies.
pub async fn get_user(
pool: &sqlx::PgPool,
cookies: CookieJar,
) -> Result<Option<users::User>, anyhow::Error> {
pub async fn get_optional_user(pool: &PgPool, cookies: CookieJar) -> Result<Option<User>> {
let token = extract_auth_token(cookies)?;
match token {
Some(token) => {
@ -29,12 +30,37 @@ pub async fn get_user(
}
}
/// Get context for templates with user field set.
pub async fn get_context(
pool: &sqlx::PgPool,
cookies: CookieJar,
) -> Result<tera::Context, anyhow::Error> {
/// Get current user by looking at request headers.
/// Returns an error if the user is not authorized.
pub async fn get_user(pool: &PgPool, cookies: CookieJar) -> Result<User, ControllerError> {
match get_optional_user(pool, cookies).await? {
Some(user) => Ok(user),
None => Err(ControllerError {
status_code: StatusCode::UNAUTHORIZED,
description: String::from("You need to log in to your account to access this page."),
details: None,
}),
}
}
/// Get current user by looking at request headers.
/// Returns an error if the user is not authorized or is not an administrator..
pub async fn get_admin_user(pool: &PgPool, cookies: CookieJar) -> Result<User, ControllerError> {
let user = get_user(pool, cookies).await?;
if !user.is_admin {
return Err(ControllerError {
status_code: StatusCode::FORBIDDEN,
description: String::from("Only administrators can access this page."),
details: None,
});
}
Ok(user)
}
/// Get context for templates with user field set.
pub async fn get_context(pool: &sqlx::PgPool, cookies: CookieJar) -> Result<tera::Context> {
let user = get_optional_user(pool, cookies).await?;
let mut ctx = tera::Context::new();
ctx.insert("user", &user);
Ok(ctx)

View file

@ -4,26 +4,36 @@ use axum::{
};
use log::error;
pub struct ControllerError(anyhow::Error);
pub struct ControllerError {
pub status_code: StatusCode,
/// Description of the error available to the user.
pub description: String,
/// Detailed description of the error that is not sent to the user.
pub details: Option<String>,
}
impl IntoResponse for ControllerError {
fn into_response(self) -> Response {
error!("{}", self.0);
(
StatusCode::INTERNAL_SERVER_ERROR,
"something went wrong, see logs for details",
)
.into_response()
if let Some(details) = self.details {
error!("{}", details);
}
(self.status_code, self.description).into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into `Result<_, AppError>`.
// That way you don't need to do that manually.
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into `Result<_, ControllerError>`.
impl<E> From<E> for ControllerError
where
E: Into<anyhow::Error>,
E: Into<anyhow::Error> + ToString,
{
fn from(err: E) -> Self {
Self(err.into())
Self {
status_code: StatusCode::INTERNAL_SERVER_ERROR,
description: String::from(
"Something went wrong, more information is available in the logs.",
),
details: Some(err.to_string()),
}
}
}

View file

@ -0,0 +1,115 @@
use axum::{
extract::{Multipart, Path, Request, State},
http::header,
response::IntoResponse,
Json,
};
use axum_extra::extract::cookie::CookieJar;
use image::ImageReader;
use serde::Serialize;
use std::{
fs::{self, File},
io::{Cursor, Write},
path,
sync::Arc,
};
use tower_http::services::ServeFile;
use uuid::Uuid;
use super::{AppState, ControllerError};
use crate::config::CONFIG;
use crate::internal::db::images;
#[derive(Serialize)]
pub struct UploadResult {
ids: Vec<Uuid>,
}
#[derive(Serialize)]
pub struct DeletionResult {
id: Uuid,
}
fn get_image_path(id: Uuid) -> path::PathBuf {
CONFIG.uploads_dir.join(format!("images/{}", id.simple()))
}
pub async fn upload_image(
State(state): State<Arc<AppState>>,
cookies: CookieJar,
mut multipart: Multipart,
) -> Result<Json<UploadResult>, ControllerError> {
super::get_admin_user(&state.db, cookies).await?;
let mut res = UploadResult { ids: Vec::new() };
while let Some(field) = multipart.next_field().await? {
let name = field.name();
if name.is_none() || name != Some("image") {
continue;
}
let id = Uuid::new_v4();
let file_path = get_image_path(id);
let original_filename = field.file_name().map(|s| s.to_string());
let data = field.bytes().await?;
let img_reader = ImageReader::new(Cursor::new(&data)).with_guessed_format()?;
let format = match img_reader.format() {
Some(format) => format,
None => continue,
};
let mime_type = format.to_mime_type().to_string();
let decoded_image = img_reader.decode()?;
let width: i32 = decoded_image.width().try_into().unwrap();
let height: i32 = decoded_image.height().try_into().unwrap();
let mut file_handle = File::create(file_path)?;
file_handle.write_all(&data)?;
let img = images::NewImage {
id,
width,
height,
mime_type,
original_filename,
alt: None,
};
images::save_image(&state.db, img).await?;
res.ids.push(id);
}
Ok(Json(res))
}
pub async fn get_image(
State(state): State<Arc<AppState>>,
Path(image_id): Path<Uuid>,
req: Request,
) -> Result<impl IntoResponse, ControllerError> {
let image_path = get_image_path(image_id);
let db_img = images::get_image(&state.db, image_id).await?;
let content = ServeFile::new(image_path).try_call(req).await?;
Ok(([(header::CONTENT_TYPE, db_img.mime_type)], content))
}
pub async fn delete_image(
State(state): State<Arc<AppState>>,
Path(image_id): Path<Uuid>,
cookies: CookieJar,
) -> Result<Json<DeletionResult>, ControllerError> {
super::get_admin_user(&state.db, cookies).await?;
images::delete_image(&state.db, image_id).await?;
let image_path = get_image_path(image_id);
fs::remove_file(image_path)?;
Ok(Json(DeletionResult { id: image_id }))
}

View file

@ -2,10 +2,12 @@ mod auth;
mod ctx;
mod error;
mod home;
mod images;
mod openid;
pub use auth::*;
pub use home::*;
pub use images::{delete_image, get_image, upload_image};
pub use openid::*;
use ctx::*;

View file

@ -1,12 +1,13 @@
use axum::{
extract::{Query, State},
http::header::SET_COOKIE,
http::{header::SET_COOKIE, StatusCode},
response::{AppendHeaders, IntoResponse, Redirect},
};
use base64::prelude::*;
use std::{collections::HashMap, sync::Arc};
use url::Url;
use super::{AppState, ControllerError};
use crate::config::CONFIG;
use crate::internal::db::users;
use crate::internal::openid;
@ -18,9 +19,7 @@ fn gen_redirect_uri() -> String {
}
/// Redirect the user to the authorization page.
pub async fn signin(
State(state): State<Arc<super::AppState>>,
) -> Result<Redirect, super::ControllerError> {
pub async fn signin(State(state): State<Arc<AppState>>) -> Result<Redirect, ControllerError> {
let raw_openid_state = users::generate_openid_state(&state.db).await?;
let openid_state = BASE64_URL_SAFE.encode(raw_openid_state);
@ -44,20 +43,22 @@ pub async fn signin(
pub async fn exchange_code(
Query(params): Query<HashMap<String, String>>,
State(state): State<Arc<super::AppState>>,
) -> Result<impl IntoResponse, super::ControllerError> {
let openid_state = params
.get("state")
.ok_or(anyhow::anyhow!("state is missing"))?;
let raw_openid_state = BASE64_URL_SAFE.decode(openid_state)?;
let state_found = users::delete_openid_state(&state.db, raw_openid_state).await?;
if !state_found {
return Err(anyhow::anyhow!("state was not found").into());
}
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, ControllerError> {
let openid_state = params.get("state").ok_or(ControllerError {
status_code: StatusCode::BAD_REQUEST,
description: String::from("Parameter 'state' was not found."),
details: None,
})?;
let code = params.get("code").ok_or(ControllerError {
status_code: StatusCode::BAD_REQUEST,
description: String::from("Parameter 'code' was not found."),
details: None,
})?;
let raw_openid_state = BASE64_URL_SAFE.decode(openid_state)?;
users::delete_openid_state(&state.db, raw_openid_state).await?;
let code = params
.get("code")
.ok_or(anyhow::anyhow!("authorization code is missing"))?;
let creds = openid::exchange_code(code, &gen_redirect_uri()).await?;
let token = openid::parse_id_token(creds.id_token).await?;