diff --git a/.gitea/workflows/pr_agent.yaml b/.gitea/workflows/pr-agent.yaml similarity index 100% rename from .gitea/workflows/pr_agent.yaml rename to .gitea/workflows/pr-agent.yaml diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 9c5f4b0..1c503c2 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "base64", + "base64 0.22.1", "bitflags", "brotli", "bytes", @@ -40,7 +40,7 @@ dependencies = [ "foldhash", "futures-core", "h2", - "http", + "http 0.2.12", "httparse", "httpdate", "itoa", @@ -114,7 +114,7 @@ checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", - "http", + "http 0.2.12", "regex", "regex-lite", "serde", @@ -158,6 +158,24 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-session" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe6976a74f34f1b6d07a6c05aadc0ed0359304a7781c367fa5b4029418db08f" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "derive_more 1.0.0", + "rand 0.8.5", + "redis", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -238,6 +256,41 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -333,6 +386,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "async-trait" version = "0.1.88" @@ -353,6 +418,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "auth" +version = "0.2.0" +dependencies = [ + "actix-session", + "actix-web", + "async-trait", + "log", + "openidconnect", + "serde", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -374,6 +451,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -469,6 +564,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.41" @@ -479,16 +580,41 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -516,7 +642,14 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", "time", "version_check", ] @@ -575,6 +708,18 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -582,9 +727,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "darling" version = "0.20.11" @@ -638,6 +820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -653,13 +836,34 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + [[package]] name = "derive_more" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "derive_more-impl", + "derive_more-impl 2.0.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", ] [[package]] @@ -709,6 +913,50 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -718,6 +966,27 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -794,6 +1063,22 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.1.1" @@ -944,6 +1229,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -953,8 +1239,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -964,9 +1252,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -975,6 +1275,17 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -986,14 +1297,20 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", - "indexmap", + "http 0.2.12", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.3" @@ -1011,7 +1328,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.3", ] [[package]] @@ -1064,6 +1381,40 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -1076,6 +1427,66 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.2", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -1252,7 +1663,6 @@ dependencies = [ "actix-multipart", "actix-web", "async-trait", - "chrono", "futures", "log", "serde", @@ -1265,6 +1675,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -1272,7 +1693,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.3", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", ] [[package]] @@ -1281,6 +1728,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1413,6 +1869,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "md-5" version = "0.10.6" @@ -1456,6 +1918,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1509,6 +1981,26 @@ dependencies = [ "libm", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.16", + "http 1.3.1", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + [[package]] name = "object" version = "0.36.7" @@ -1524,6 +2016,76 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http 1.3.1", + "itertools", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.1" @@ -1574,6 +2136,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1613,6 +2195,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.0" @@ -1655,6 +2249,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1664,6 +2267,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -1738,6 +2396,29 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "redis" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e902a69d09078829137b4a5d9d082e0490393537badd7c91a3d69d14639e115f" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "tokio", + "tokio-retry", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.5.12" @@ -1747,6 +2428,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.11.1" @@ -1782,6 +2483,56 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.2", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -1822,6 +2573,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1863,6 +2620,9 @@ name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -1887,12 +2647,50 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "semver" version = "1.0.26" @@ -1908,6 +2706,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -1931,6 +2739,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_plain" version = "1.0.2" @@ -1952,14 +2770,50 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "server" version = "0.2.0" dependencies = [ + "actix-session", "actix-web", + "auth", "dotenv", "env_logger", + "hex", "image", + "openidconnect", "percent-encoding", "post", "sqlx", @@ -2078,7 +2932,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "chrono", "crc", @@ -2089,9 +2943,9 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", + "hashbrown 0.15.3", "hashlink", - "indexmap", + "indexmap 2.9.0", "log", "memchr", "once_cell", @@ -2101,12 +2955,12 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror", + "thiserror 2.0.12", "tokio", "tokio-stream", "tracing", "url", - "webpki-roots", + "webpki-roots 0.26.10", ] [[package]] @@ -2155,7 +3009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -2186,7 +3040,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.12", "tracing", "whoami", ] @@ -2198,7 +3052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "chrono", @@ -2224,7 +3078,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.12", "tracing", "whoami", ] @@ -2249,7 +3103,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.12", "tracing", "url", ] @@ -2294,6 +3148,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2318,13 +3181,33 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2411,6 +3294,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -2435,6 +3339,51 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -2467,6 +3416,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.18.0" @@ -2506,6 +3461,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -2521,6 +3486,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -2553,6 +3519,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2600,6 +3575,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -2632,6 +3620,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.10" @@ -2641,6 +3649,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "whoami" version = "1.6.0" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7b4f8db..272ab65 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["server", "feature/image", "feature/post"] +members = ["server", "feature/auth", "feature/image", "feature/post"] resolver = "2" [workspace.package] @@ -8,13 +8,19 @@ edition = "2024" [workspace.dependencies] actix-multipart = "0.7.2" +actix-session = { version = "0.10.1", features = ["redis-session"] } actix-web = "4.10.2" async-trait = "0.1.88" chrono = "0.4.41" dotenv = "0.15.0" env_logger = "0.11.8" futures = "0.3.31" +hex = "0.4.3" log = "0.4.27" +openidconnect = { version = "4.0.1", features = [ + "reqwest", + "reqwest-blocking", +] } percent-encoding = "2.3.1" serde = { version = "1.0.219", features = ["derive"] } sqlx = { version = "0.8.5", features = [ @@ -26,5 +32,6 @@ sqlx = { version = "0.8.5", features = [ tokio = { version = "1.45.0", features = ["full"] } server.path = "server" +auth.path = "feature/auth" image.path = "feature/image" post.path = "feature/post" diff --git a/backend/Dockerfile b/backend/Dockerfile index e67f7ae..2b90549 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -21,4 +21,11 @@ ENV DATABASE_PORT=5432 ENV DATABASE_USER=postgres ENV DATABASE_PASSWORD= ENV DATABASE_NAME=postgres +ENV REIDS_URL=redis://127.0.0.1:6379 +ENV SESSION_KEY='64-bytes-hex-string-which-can-be-generated-by-`openssl rand -hex 64`' +ENV OIDC_ISSUER_URL= +ENV OIDC_REDIRECT_URL= +ENV OIDC_CLIENT_ID= +ENV OIDC_CLIENT_SECRET= + CMD ["./server"] diff --git a/backend/feature/auth/Cargo.toml b/backend/feature/auth/Cargo.toml new file mode 100644 index 0000000..26ffa18 --- /dev/null +++ b/backend/feature/auth/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "auth" +version.workspace = true +edition.workspace = true + +[dependencies] +actix-session.workspace = true +actix-web.workspace = true +async-trait.workspace = true +log.workspace = true +openidconnect.workspace = true +serde.workspace = true diff --git a/backend/feature/auth/src/adapter.rs b/backend/feature/auth/src/adapter.rs new file mode 100644 index 0000000..a37dc94 --- /dev/null +++ b/backend/feature/auth/src/adapter.rs @@ -0,0 +1,2 @@ +pub mod delivery; +pub mod gateway; \ No newline at end of file diff --git a/backend/feature/auth/src/adapter/delivery.rs b/backend/feature/auth/src/adapter/delivery.rs new file mode 100644 index 0000000..3ab749e --- /dev/null +++ b/backend/feature/auth/src/adapter/delivery.rs @@ -0,0 +1,3 @@ +pub mod auth_controller; +pub mod oidc_callback_query_dto; +pub mod user_response_dto; diff --git a/backend/feature/auth/src/adapter/delivery/auth_controller.rs b/backend/feature/auth/src/adapter/delivery/auth_controller.rs new file mode 100644 index 0000000..0171f6b --- /dev/null +++ b/backend/feature/auth/src/adapter/delivery/auth_controller.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + adapter::delivery::{ + oidc_callback_query_dto::OidcCallbackQueryDto, user_response_dto::UserResponseDto, + }, + application::{ + error::auth_error::AuthError, + use_case::{ + exchange_auth_code_use_case::ExchangeAuthCodeUseCase, + get_auth_url_use_case::{AuthUrl, GetAuthUrlUseCase}, + }, + }, +}; + +#[async_trait] +pub trait AuthController: Send + Sync { + fn oidc_login(&self) -> Result; + + async fn oidc_callback( + &self, + query: OidcCallbackQueryDto, + expected_state: &str, + expected_nonce: &str, + ) -> Result; +} + +pub struct AuthControllerImpl { + get_auth_url_use_case: Arc, + exchange_auth_code_use_case: Arc, +} + +impl AuthControllerImpl { + pub fn new( + get_auth_url_use_case: Arc, + exchange_auth_code_use_case: Arc, + ) -> Self { + Self { + get_auth_url_use_case, + exchange_auth_code_use_case, + } + } +} + +#[async_trait] +impl AuthController for AuthControllerImpl { + fn oidc_login(&self) -> Result { + self.get_auth_url_use_case.execute() + } + + async fn oidc_callback( + &self, + query: OidcCallbackQueryDto, + expected_state: &str, + expected_nonce: &str, + ) -> Result { + let result = self + .exchange_auth_code_use_case + .execute(&query.code, &query.state, expected_state, expected_nonce) + .await; + + result.map(|user| UserResponseDto::from(user)) + } +} diff --git a/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs b/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs new file mode 100644 index 0000000..5cf4eb9 --- /dev/null +++ b/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct OidcCallbackQueryDto { + pub code: String, + pub state: String, +} diff --git a/backend/feature/auth/src/adapter/delivery/user_response_dto.rs b/backend/feature/auth/src/adapter/delivery/user_response_dto.rs new file mode 100644 index 0000000..975ab8c --- /dev/null +++ b/backend/feature/auth/src/adapter/delivery/user_response_dto.rs @@ -0,0 +1,20 @@ +use serde::Serialize; + +use crate::domain::entity::user::User; + +#[derive(Serialize)] +pub struct UserResponseDto { + pub source_id: String, + pub displayed_name: String, + pub email: String, +} + +impl From for UserResponseDto { + fn from(user: User) -> Self { + UserResponseDto { + source_id: user.source_id, + displayed_name: user.displayed_name, + email: user.email, + } + } +} diff --git a/backend/feature/auth/src/adapter/gateway.rs b/backend/feature/auth/src/adapter/gateway.rs new file mode 100644 index 0000000..d813de5 --- /dev/null +++ b/backend/feature/auth/src/adapter/gateway.rs @@ -0,0 +1,3 @@ +pub mod oidc_claims_response_dto; +pub mod auth_oidc_service; +pub mod auth_repository_impl; diff --git a/backend/feature/auth/src/adapter/gateway/auth_oidc_service.rs b/backend/feature/auth/src/adapter/gateway/auth_oidc_service.rs new file mode 100644 index 0000000..85e7e59 --- /dev/null +++ b/backend/feature/auth/src/adapter/gateway/auth_oidc_service.rs @@ -0,0 +1,16 @@ +use async_trait::async_trait; + +use crate::{ + adapter::gateway::oidc_claims_response_dto::OidcClaimsResponseDto, + application::{error::auth_error::AuthError, use_case::get_auth_url_use_case::AuthUrl}, +}; + +#[async_trait] +pub trait AuthOidcService: Send + Sync { + fn get_auth_url(&self) -> Result; + async fn exchange_auth_code( + &self, + code: &str, + expected_nonce: &str, + ) -> Result; +} diff --git a/backend/feature/auth/src/adapter/gateway/auth_repository_impl.rs b/backend/feature/auth/src/adapter/gateway/auth_repository_impl.rs new file mode 100644 index 0000000..533da3a --- /dev/null +++ b/backend/feature/auth/src/adapter/gateway/auth_repository_impl.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + adapter::gateway::auth_oidc_service::AuthOidcService, + application::{ + error::auth_error::AuthError, gateway::auth_repository::AuthRepository, + use_case::get_auth_url_use_case::AuthUrl, + }, + domain::entity::user::User, +}; + +pub struct AuthRepositoryImpl { + auth_oidc_service: Arc, +} + +impl AuthRepositoryImpl { + pub fn new(auth_oidc_service: Arc) -> Self { + Self { auth_oidc_service } + } +} + +#[async_trait] +impl AuthRepository for AuthRepositoryImpl { + fn get_auth_url(&self) -> Result { + self.auth_oidc_service.get_auth_url() + } + + async fn exchange_auth_code( + &self, + code: &str, + expected_nonce: &str, + ) -> Result { + self.auth_oidc_service + .exchange_auth_code(code, expected_nonce) + .await + .map(|dto| dto.into_entity()) + } +} diff --git a/backend/feature/auth/src/adapter/gateway/oidc_claims_response_dto.rs b/backend/feature/auth/src/adapter/gateway/oidc_claims_response_dto.rs new file mode 100644 index 0000000..94c17b7 --- /dev/null +++ b/backend/feature/auth/src/adapter/gateway/oidc_claims_response_dto.rs @@ -0,0 +1,17 @@ +use crate::domain::entity::user::User; + +pub struct OidcClaimsResponseDto { + pub sub: String, + pub preferred_username: Option, + pub email: Option, +} + +impl OidcClaimsResponseDto { + pub fn into_entity(self) -> User { + User { + source_id: self.sub, + displayed_name: self.preferred_username.unwrap_or_default(), + email: self.email.unwrap_or_default(), + } + } +} diff --git a/backend/feature/auth/src/application.rs b/backend/feature/auth/src/application.rs new file mode 100644 index 0000000..f24625e --- /dev/null +++ b/backend/feature/auth/src/application.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod gateway; +pub mod use_case; diff --git a/backend/feature/auth/src/application/error.rs b/backend/feature/auth/src/application/error.rs new file mode 100644 index 0000000..6115091 --- /dev/null +++ b/backend/feature/auth/src/application/error.rs @@ -0,0 +1 @@ +pub mod auth_error; \ No newline at end of file diff --git a/backend/feature/auth/src/application/error/auth_error.rs b/backend/feature/auth/src/application/error/auth_error.rs new file mode 100644 index 0000000..820a405 --- /dev/null +++ b/backend/feature/auth/src/application/error/auth_error.rs @@ -0,0 +1,8 @@ +#[derive(Debug, PartialEq)] +pub enum AuthError { + OidcError(String), + InvalidState, + InvalidNonce, + InvalidAuthCode, + InvalidIdToken, +} diff --git a/backend/feature/auth/src/application/gateway.rs b/backend/feature/auth/src/application/gateway.rs new file mode 100644 index 0000000..cd1cf22 --- /dev/null +++ b/backend/feature/auth/src/application/gateway.rs @@ -0,0 +1 @@ +pub mod auth_repository; diff --git a/backend/feature/auth/src/application/gateway/auth_repository.rs b/backend/feature/auth/src/application/gateway/auth_repository.rs new file mode 100644 index 0000000..98a12f5 --- /dev/null +++ b/backend/feature/auth/src/application/gateway/auth_repository.rs @@ -0,0 +1,13 @@ +use async_trait::async_trait; + +use crate::{ + application::{error::auth_error::AuthError, use_case::get_auth_url_use_case::AuthUrl}, + domain::entity::user::User, +}; + +#[async_trait] +pub trait AuthRepository: Send + Sync { + fn get_auth_url(&self) -> Result; + async fn exchange_auth_code(&self, code: &str, expected_nonce: &str) + -> Result; +} diff --git a/backend/feature/auth/src/application/use_case.rs b/backend/feature/auth/src/application/use_case.rs new file mode 100644 index 0000000..7312b52 --- /dev/null +++ b/backend/feature/auth/src/application/use_case.rs @@ -0,0 +1,2 @@ +pub mod exchange_auth_code_use_case; +pub mod get_auth_url_use_case; diff --git a/backend/feature/auth/src/application/use_case/exchange_auth_code_use_case.rs b/backend/feature/auth/src/application/use_case/exchange_auth_code_use_case.rs new file mode 100644 index 0000000..65e90d1 --- /dev/null +++ b/backend/feature/auth/src/application/use_case/exchange_auth_code_use_case.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::{ + application::{error::auth_error::AuthError, gateway::auth_repository::AuthRepository}, + domain::entity::user::User, +}; + +#[async_trait] +pub trait ExchangeAuthCodeUseCase: Send + Sync { + async fn execute( + &self, + code: &str, + received_state: &str, + expected_state: &str, + expected_nonce: &str, + ) -> Result; +} + +pub struct ExchangeAuthCodeUseCaseImpl { + auth_repository: Arc, +} + +impl ExchangeAuthCodeUseCaseImpl { + pub fn new(auth_repository: Arc) -> Self { + Self { auth_repository } + } +} + +#[async_trait] +impl ExchangeAuthCodeUseCase for ExchangeAuthCodeUseCaseImpl { + async fn execute( + &self, + code: &str, + received_state: &str, + expected_state: &str, + expected_nonce: &str, + ) -> Result { + if received_state != expected_state { + return Err(AuthError::InvalidState); + } + + self.auth_repository + .exchange_auth_code(code, expected_nonce) + .await + } +} diff --git a/backend/feature/auth/src/application/use_case/get_auth_url_use_case.rs b/backend/feature/auth/src/application/use_case/get_auth_url_use_case.rs new file mode 100644 index 0000000..7ad0be2 --- /dev/null +++ b/backend/feature/auth/src/application/use_case/get_auth_url_use_case.rs @@ -0,0 +1,29 @@ +use std::sync::Arc; + +use crate::application::{error::auth_error::AuthError, gateway::auth_repository::AuthRepository}; + +pub trait GetAuthUrlUseCase: Send + Sync { + fn execute(&self) -> Result; +} + +pub struct LoginUseCaseImpl { + auth_repository: Arc, +} + +impl LoginUseCaseImpl { + pub fn new(auth_repository: Arc) -> Self { + Self { auth_repository } + } +} + +impl GetAuthUrlUseCase for LoginUseCaseImpl { + fn execute(&self) -> Result { + self.auth_repository.get_auth_url() + } +} + +pub struct AuthUrl { + pub url: String, + pub state: String, + pub nonce: String, +} diff --git a/backend/feature/auth/src/domain.rs b/backend/feature/auth/src/domain.rs new file mode 100644 index 0000000..bccca66 --- /dev/null +++ b/backend/feature/auth/src/domain.rs @@ -0,0 +1 @@ +pub mod entity; \ No newline at end of file diff --git a/backend/feature/auth/src/domain/entity.rs b/backend/feature/auth/src/domain/entity.rs new file mode 100644 index 0000000..018ff2e --- /dev/null +++ b/backend/feature/auth/src/domain/entity.rs @@ -0,0 +1 @@ +pub mod user; \ No newline at end of file diff --git a/backend/feature/auth/src/domain/entity/user.rs b/backend/feature/auth/src/domain/entity/user.rs new file mode 100644 index 0000000..2cd83ca --- /dev/null +++ b/backend/feature/auth/src/domain/entity/user.rs @@ -0,0 +1,5 @@ +pub struct User { + pub source_id: String, + pub displayed_name: String, + pub email: String, +} diff --git a/backend/feature/auth/src/framework.rs b/backend/feature/auth/src/framework.rs new file mode 100644 index 0000000..39c6581 --- /dev/null +++ b/backend/feature/auth/src/framework.rs @@ -0,0 +1,2 @@ +pub mod oidc; +pub mod web; diff --git a/backend/feature/auth/src/framework/oidc.rs b/backend/feature/auth/src/framework/oidc.rs new file mode 100644 index 0000000..a8d90c5 --- /dev/null +++ b/backend/feature/auth/src/framework/oidc.rs @@ -0,0 +1 @@ +pub mod auth_oidc_service_impl; diff --git a/backend/feature/auth/src/framework/oidc/auth_oidc_service_impl.rs b/backend/feature/auth/src/framework/oidc/auth_oidc_service_impl.rs new file mode 100644 index 0000000..d60f7da --- /dev/null +++ b/backend/feature/auth/src/framework/oidc/auth_oidc_service_impl.rs @@ -0,0 +1,108 @@ +use async_trait::async_trait; +use openidconnect::{ + AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndpointMaybeSet, EndpointNotSet, + EndpointSet, Nonce, RedirectUrl, TokenResponse as _, + core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata}, +}; + +use crate::{ + adapter::gateway::{ + auth_oidc_service::AuthOidcService, oidc_claims_response_dto::OidcClaimsResponseDto, + }, + application::{error::auth_error::AuthError, use_case::get_auth_url_use_case::AuthUrl}, +}; + +type CompleteClient< + HasAuthUrl = EndpointSet, + HasDeviceAuthUrl = EndpointNotSet, + HasIntrospectionUrl = EndpointNotSet, + HasRevocationUrl = EndpointNotSet, + HasTokenUrl = EndpointMaybeSet, + HasUserInfoUrl = EndpointMaybeSet, +> = CoreClient< + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + HasUserInfoUrl, +>; + +pub struct AuthOidcServiceImpl { + oidc_client: CompleteClient, + http_client: openidconnect::reqwest::Client, +} + +impl AuthOidcServiceImpl { + pub fn new( + provider_metadata: CoreProviderMetadata, + client_id: &str, + client_secret: &str, + redirect_url: RedirectUrl, + http_client: openidconnect::reqwest::Client, + ) -> Self { + Self { + oidc_client: CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(client_id.to_string()), + Some(ClientSecret::new(client_secret.to_string())), + ) + .set_redirect_uri(redirect_url), + http_client, + } + } +} + +#[async_trait] +impl AuthOidcService for AuthOidcServiceImpl { + fn get_auth_url(&self) -> Result { + let (url, state, nonce) = self + .oidc_client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .url(); + + Ok(AuthUrl { + url: url.to_string(), + state: state.secret().into(), + nonce: nonce.secret().into(), + }) + } + + async fn exchange_auth_code( + &self, + code: &str, + expected_nonce: &str, + ) -> Result { + let token_response = self + .oidc_client + .exchange_code(AuthorizationCode::new(code.to_string())) + .map_err(|e| AuthError::OidcError(e.to_string()))? + .request_async(&self.http_client) + .await + .map_err(|_| AuthError::InvalidAuthCode)?; + + let id_token = token_response.id_token().ok_or(AuthError::InvalidIdToken)?; + let claims = id_token + .claims( + &self.oidc_client.id_token_verifier(), + &Nonce::new(expected_nonce.to_string()), + ) + .map_err(|_| AuthError::InvalidIdToken)?; + + let preferred_username = claims + .preferred_username() + .map(|username| username.to_string()); + + let email = claims.email().map(|email| email.to_string()); + + Ok(OidcClaimsResponseDto { + sub: claims.subject().to_string(), + preferred_username: preferred_username, + email: email, + }) + } +} diff --git a/backend/feature/auth/src/framework/web.rs b/backend/feature/auth/src/framework/web.rs new file mode 100644 index 0000000..60c8eb8 --- /dev/null +++ b/backend/feature/auth/src/framework/web.rs @@ -0,0 +1 @@ +pub mod auth_web_routes; diff --git a/backend/feature/auth/src/framework/web/auth_web_routes.rs b/backend/feature/auth/src/framework/web/auth_web_routes.rs new file mode 100644 index 0000000..c56dda3 --- /dev/null +++ b/backend/feature/auth/src/framework/web/auth_web_routes.rs @@ -0,0 +1,102 @@ +use actix_session::Session; +use actix_web::{HttpResponse, Responder, http::header, web}; + +use crate::{ + adapter::delivery::{ + auth_controller::AuthController, oidc_callback_query_dto::OidcCallbackQueryDto, + }, + application::error::auth_error::AuthError, +}; + +const SESSION_KEY_AUTH_STATE: &str = "auth_state"; +const SESSION_KEY_AUTH_NONCE: &str = "auth_nonce"; +const SESSION_KEY_USER: &str = "user"; + +pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/auth") + .route("/login", web::get().to(oidc_login_handler)) + .route("/callback", web::get().to(oidc_callback_handler)) + .route("/logout", web::get().to(logout_handler)), + ); +} + +async fn oidc_login_handler( + auth_controller: web::Data, + session: Session, +) -> impl Responder { + let result = auth_controller.oidc_login(); + + match result { + Ok(auth_url) => { + if let Err(e) = session.insert(SESSION_KEY_AUTH_STATE, auth_url.state) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + if let Err(e) = session.insert(SESSION_KEY_AUTH_NONCE, auth_url.nonce) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + HttpResponse::Found() + .append_header((header::LOCATION, auth_url.url)) + .finish() + } + Err(e) => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + } +} + +async fn oidc_callback_handler( + auth_controller: web::Data, + query: web::Query, + session: Session, +) -> impl Responder { + let expected_state: String = match session.get(SESSION_KEY_AUTH_STATE) { + Ok(Some(state)) => state, + _ => return HttpResponse::BadRequest().finish(), + }; + + let expected_nonce: String = match session.get(SESSION_KEY_AUTH_NONCE) { + Ok(Some(nonce)) => nonce, + _ => return HttpResponse::BadRequest().finish(), + }; + + let result = auth_controller + .oidc_callback(query.into_inner(), &expected_state, &expected_nonce) + .await; + + session.remove(SESSION_KEY_AUTH_STATE); + session.remove(SESSION_KEY_AUTH_NONCE); + match result { + Ok(user) => { + if let Err(e) = session.insert(SESSION_KEY_USER, user) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + HttpResponse::Found() + .append_header((header::LOCATION, "/")) + .finish() + } + Err(e) => match e { + AuthError::InvalidAuthCode + | AuthError::InvalidIdToken + | AuthError::InvalidNonce + | AuthError::InvalidState => HttpResponse::BadRequest().finish(), + _ => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + }, + } +} + +async fn logout_handler(session: Session) -> impl Responder { + session.remove(SESSION_KEY_AUTH_STATE); + session.remove(SESSION_KEY_AUTH_NONCE); + session.remove(SESSION_KEY_USER); + HttpResponse::Found() + .append_header((header::LOCATION, "/")) + .finish() +} diff --git a/backend/feature/auth/src/lib.rs b/backend/feature/auth/src/lib.rs new file mode 100644 index 0000000..062f800 --- /dev/null +++ b/backend/feature/auth/src/lib.rs @@ -0,0 +1,4 @@ +pub mod adapter; +pub mod application; +pub mod domain; +pub mod framework; diff --git a/backend/feature/image/Cargo.toml b/backend/feature/image/Cargo.toml index 07a7f78..0e6fc35 100644 --- a/backend/feature/image/Cargo.toml +++ b/backend/feature/image/Cargo.toml @@ -7,7 +7,6 @@ edition.workspace = true actix-multipart.workspace = true actix-web.workspace = true async-trait.workspace = true -chrono.workspace = true futures.workspace = true log.workspace = true serde.workspace = true diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index b8e4471..938f82e 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -4,11 +4,15 @@ version.workspace = true edition.workspace = true [dependencies] +actix-session.workspace = true actix-web.workspace = true dotenv.workspace = true env_logger.workspace = true +hex.workspace = true +openidconnect.workspace = true percent-encoding.workspace = true sqlx.workspace = true +auth.workspace = true image.workspace = true post.workspace = true diff --git a/backend/server/src/configuration.rs b/backend/server/src/configuration.rs new file mode 100644 index 0000000..3647d44 --- /dev/null +++ b/backend/server/src/configuration.rs @@ -0,0 +1,33 @@ +use openidconnect::reqwest; + +use crate::configuration::{ + db::DbConfiguration, oidc::OidcConfiguration, server::ServerConfiguration, + session::SessionConfiguration, storage::StorageConfiguration, +}; + +pub mod db; +pub mod oidc; +pub mod server; +pub mod session; +pub mod storage; + +#[derive(Clone)] +pub struct Configuration { + pub db: DbConfiguration, + pub oidc: OidcConfiguration, + pub server: ServerConfiguration, + pub session: SessionConfiguration, + pub storage: StorageConfiguration, +} + +impl Configuration { + pub async fn new(http_client: reqwest::Client) -> Self { + Self { + db: DbConfiguration::new(), + oidc: OidcConfiguration::new(http_client).await, + server: ServerConfiguration::new(), + session: SessionConfiguration::new(), + storage: StorageConfiguration::new(), + } + } +} diff --git a/backend/server/src/configuration/db.rs b/backend/server/src/configuration/db.rs new file mode 100644 index 0000000..4014f82 --- /dev/null +++ b/backend/server/src/configuration/db.rs @@ -0,0 +1,44 @@ +use std::env; + +use sqlx::{Pool, Postgres, postgres::PgPoolOptions}; + +#[derive(Clone)] +pub struct DbConfiguration { + pub database_url: String, +} + +impl DbConfiguration { + pub fn new() -> Self { + let host = env::var("DATABASE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + let port = env::var("DATABASE_PORT").unwrap_or_else(|_| "5432".to_string()); + let user = env::var("DATABASE_USER").unwrap_or_else(|_| "postgres".to_string()); + let password = env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "".to_string()); + let dbname = env::var("DATABASE_NAME").unwrap_or_else(|_| "postgres".to_string()); + + let encoded_password = + percent_encoding::utf8_percent_encode(&password, percent_encoding::NON_ALPHANUMERIC) + .to_string(); + + let database_url = format!( + "postgres://{}:{}@{}:{}/{}", + user, encoded_password, host, port, dbname + ); + + Self { database_url } + } + + pub async fn create_connection(&self) -> Pool { + let db_pool = PgPoolOptions::new() + .max_connections(5) + .connect(&self.database_url) + .await + .expect("Failed to create database connection pool"); + + sqlx::migrate!("../migrations") + .run(&db_pool) + .await + .expect("Failed to run database migrations"); + + db_pool + } +} diff --git a/backend/server/src/configuration/oidc.rs b/backend/server/src/configuration/oidc.rs new file mode 100644 index 0000000..7577522 --- /dev/null +++ b/backend/server/src/configuration/oidc.rs @@ -0,0 +1,35 @@ +use std::env; + +use openidconnect::{IssuerUrl, RedirectUrl, core::CoreProviderMetadata, reqwest}; + +#[derive(Clone)] +pub struct OidcConfiguration { + pub provider_metadata: CoreProviderMetadata, + pub client_id: String, + pub client_secret: String, + pub redirect_url: RedirectUrl, +} + +impl OidcConfiguration { + pub async fn new(http_client: reqwest::Client) -> Self { + let issuer_url = env::var("OIDC_ISSUER_URL").expect("OIDC_ISSUER_URL must be set"); + let client_id = env::var("OIDC_CLIENT_ID").expect("OIDC_CLIENT_ID must be set"); + let client_secret = env::var("OIDC_CLIENT_SECRET").expect("OIDC_CLIENT_SECRET must be set"); + let redirect_url_str = env::var("OIDC_REDIRECT_URL") + .unwrap_or_else(|_| "http://127.0.0.1:8080/auth/callback".to_string()); + + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(issuer_url).expect("Invalid issuer URL"), + &http_client, + ) + .await + .expect("Failed to discover OIDC provider metadata"); + + Self { + provider_metadata, + client_id, + client_secret, + redirect_url: RedirectUrl::new(redirect_url_str).expect("Invalid redirect URI"), + } + } +} diff --git a/backend/server/src/configuration/server.rs b/backend/server/src/configuration/server.rs new file mode 100644 index 0000000..34bb696 --- /dev/null +++ b/backend/server/src/configuration/server.rs @@ -0,0 +1,17 @@ +#[derive(Clone)] +pub struct ServerConfiguration { + pub host: String, + pub port: u16, +} + +impl ServerConfiguration { + pub fn new() -> Self { + let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + let port = std::env::var("PORT") + .unwrap_or_else(|_| "8080".to_string()) + .parse::() + .unwrap(); + + Self { host, port } + } +} diff --git a/backend/server/src/configuration/session.rs b/backend/server/src/configuration/session.rs new file mode 100644 index 0000000..5eb8ff1 --- /dev/null +++ b/backend/server/src/configuration/session.rs @@ -0,0 +1,36 @@ +use actix_session::storage::RedisSessionStore; +use actix_web::cookie::Key; + +#[derive(Clone)] +pub struct SessionConfiguration { + pub session_key: Key, + pub redis_url: String, +} + +impl SessionConfiguration { + pub fn new() -> Self { + let session_key_hex = std::env::var("SESSION_KEY").expect("SESSION_KEY must be set"); + let session_key_bytes = + hex::decode(session_key_hex).expect("Invalid SESSION_KEY format, must be hex encoded"); + + if session_key_bytes.len() != 64 { + panic!("SESSION_KEY must be 64 bytes long"); + } + + let session_key = Key::from(&session_key_bytes); + + let redis_url = + std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.1:6379".to_string()); + + Self { + session_key, + redis_url, + } + } + + pub async fn create_session_store(&self) -> RedisSessionStore { + RedisSessionStore::new(self.redis_url.clone()) + .await + .expect("Failed to create Redis session store") + } +} diff --git a/backend/server/src/configuration/storage.rs b/backend/server/src/configuration/storage.rs new file mode 100644 index 0000000..10dc3c5 --- /dev/null +++ b/backend/server/src/configuration/storage.rs @@ -0,0 +1,13 @@ +use std::env; + +#[derive(Clone)] +pub struct StorageConfiguration { + pub storage_path: String, +} + +impl StorageConfiguration { + pub fn new() -> Self { + let storage_path = env::var("STORAGE_PATH").unwrap_or_else(|_| "static".to_string()); + Self { storage_path } + } +} diff --git a/backend/server/src/container.rs b/backend/server/src/container.rs index 552c60c..f9a04b2 100644 --- a/backend/server/src/container.rs +++ b/backend/server/src/container.rs @@ -1,5 +1,16 @@ use std::sync::Arc; +use auth::{ + adapter::{ + delivery::auth_controller::{AuthController, AuthControllerImpl}, + gateway::auth_repository_impl::AuthRepositoryImpl, + }, + application::use_case::{ + exchange_auth_code_use_case::ExchangeAuthCodeUseCaseImpl, + get_auth_url_use_case::LoginUseCaseImpl, + }, + framework::oidc::auth_oidc_service_impl::AuthOidcServiceImpl, +}; use image::{ adapter::{ delivery::image_controller::{ImageController, ImageControllerImpl}, @@ -13,6 +24,7 @@ use image::{ storage::image_storage_impl::ImageStorageImpl, }, }; +use openidconnect::reqwest; use post::{ adapter::{ delivery::post_controller::{PostController, PostControllerImpl}, @@ -26,13 +38,37 @@ use post::{ }; use sqlx::{Pool, Postgres}; +use crate::configuration::Configuration; + pub struct Container { - pub post_controller: Arc, + pub auth_controller: Arc, pub image_controller: Arc, + pub post_controller: Arc, } impl Container { - pub fn new(db_pool: Pool, storage_path: &str) -> Self { + pub fn new( + db_pool: Pool, + http_client: reqwest::Client, + configuration: Configuration, + ) -> Self { + let oidc_configuration = &configuration.oidc; + let auth_oidc_service = Arc::new(AuthOidcServiceImpl::new( + oidc_configuration.provider_metadata.clone(), + &oidc_configuration.client_id, + &oidc_configuration.client_secret, + oidc_configuration.redirect_url.clone(), + http_client, + )); + let auth_repository = Arc::new(AuthRepositoryImpl::new(auth_oidc_service)); + let get_auth_url_use_case = Arc::new(LoginUseCaseImpl::new(auth_repository.clone())); + let exchange_auth_code_use_case = + Arc::new(ExchangeAuthCodeUseCaseImpl::new(auth_repository.clone())); + let auth_controller = Arc::new(AuthControllerImpl::new( + get_auth_url_use_case, + exchange_auth_code_use_case, + )); + let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone())); let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone())); let get_all_post_info_use_case = @@ -44,7 +80,7 @@ impl Container { )); let image_db_service = Arc::new(ImageDbServiceImpl::new(db_pool.clone())); - let image_storage = Arc::new(ImageStorageImpl::new(storage_path.into())); + let image_storage = Arc::new(ImageStorageImpl::new(&configuration.storage.storage_path)); let image_repository = Arc::new(ImageRepositoryImpl::new( image_db_service.clone(), image_storage.clone(), @@ -57,8 +93,9 @@ impl Container { )); Self { - post_controller, + auth_controller, image_controller, + post_controller, } } } diff --git a/backend/server/src/lib.rs b/backend/server/src/lib.rs index 18581c4..18fb4fc 100644 --- a/backend/server/src/lib.rs +++ b/backend/server/src/lib.rs @@ -1 +1,2 @@ +pub mod configuration; pub mod container; diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 2c63851..17fe620 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -1,67 +1,56 @@ +use actix_session::{ + SessionMiddleware, config::SessionMiddlewareBuilder, storage::RedisSessionStore, +}; use actix_web::{ App, Error, HttpServer, body::MessageBody, dev::{ServiceFactory, ServiceRequest, ServiceResponse}, web, }; +use auth::framework::web::auth_web_routes::configure_auth_routes; use image::framework::web::image_web_routes::configure_image_routes; +use openidconnect::reqwest; use post::framework::web::post_web_routes::configure_post_routes; -use server::container::Container; -use sqlx::{Pool, Postgres, postgres::PgPoolOptions}; -use std::env; +use server::{configuration::Configuration, container::Container}; +use sqlx::{Pool, Postgres}; #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv::dotenv().ok(); env_logger::init(); - let db_pool = init_database().await; - let storage_path = env::var("STORAGE_PATH").unwrap_or_else(|_| "static".to_string()); + let http_client = reqwest::ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .build() + .expect("Failed to create HTTP client"); - let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); - let port = env::var("PORT") - .unwrap_or_else(|_| "8080".to_string()) - .parse::() - .unwrap(); + let configuration = Configuration::new(http_client.clone()).await; - HttpServer::new(move || create_app(db_pool.clone(), storage_path.clone())) - .bind((host, port))? - .run() - .await -} + let host = configuration.server.host.clone(); + let port = configuration.server.port; -async fn init_database() -> Pool { - let host = env::var("DATABASE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); - let port = env::var("DATABASE_PORT").unwrap_or_else(|_| "5432".to_string()); - let user = env::var("DATABASE_USER").unwrap_or_else(|_| "postgres".to_string()); - let password = env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "".to_string()); - let dbname = env::var("DATABASE_NAME").unwrap_or_else(|_| "postgres".to_string()); + let db_pool = configuration.db.create_connection().await; + let session_key = configuration.session.session_key.clone(); + let session_store = configuration.session.create_session_store().await; - let encoded_password = - percent_encoding::utf8_percent_encode(&password, percent_encoding::NON_ALPHANUMERIC) - .to_string(); - let database_url = format!( - "postgres://{}:{}@{}:{}/{}", - user, encoded_password, host, port, dbname - ); - - let db_pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await - .expect("Failed to create database connection pool"); - - sqlx::migrate!("../migrations") - .run(&db_pool) - .await - .expect("Failed to run database migrations"); - - db_pool + HttpServer::new(move || { + create_app( + db_pool.clone(), + http_client.clone(), + SessionMiddleware::builder(session_store.clone(), session_key.clone()), + configuration.clone(), + ) + }) + .bind((host, port))? + .run() + .await } fn create_app( db_pool: Pool, - storage_path: String, + http_client: reqwest::Client, + session_middleware_builder: SessionMiddlewareBuilder, + configuration: Configuration, ) -> App< impl ServiceFactory< ServiceRequest, @@ -71,11 +60,14 @@ fn create_app( Error = Error, >, > { - let container = Container::new(db_pool, &storage_path); + let container = Container::new(db_pool, http_client, configuration); App::new() - .app_data(web::Data::from(container.post_controller)) + .wrap(session_middleware_builder.build()) + .app_data(web::Data::from(container.auth_controller)) .app_data(web::Data::from(container.image_controller)) - .configure(configure_post_routes) + .app_data(web::Data::from(container.post_controller)) + .configure(configure_auth_routes) .configure(configure_image_routes) + .configure(configure_post_routes) }